/**
 * 플로우 프로시저 호출 서비스
 * 내부/외부 DB의 프로시저/함수 목록 조회, 파라미터 조회, 실행을 담당
 */

import db from "../database/db";
import {
  getExternalPool,
  executeExternalQuery,
} from "./externalDbHelper";
import { getPlaceholder } from "./dbQueryBuilder";
import {
  FlowProcedureConfig,
  FlowProcedureParam,
  ProcedureListItem,
  ProcedureParameterInfo,
} from "../types/flow";

export class FlowProcedureService {
  /**
   * 프로시저/함수 목록 조회
   * information_schema.routines에서 사용 가능한 프로시저/함수를 가져온다
   */
  async listProcedures(
    dbSource: "internal" | "external",
    connectionId?: number,
    schema?: string
  ): Promise<ProcedureListItem[]> {
    if (dbSource === "external" && connectionId) {
      return this.listExternalProcedures(connectionId, schema);
    }
    return this.listInternalProcedures(schema);
  }

  private async listInternalProcedures(
    schema?: string
  ): Promise<ProcedureListItem[]> {
    const targetSchema = schema || "public";
    // 트리거 함수(data_type='trigger')는 직접 호출 대상이 아니므로 제외
    const query = `
      SELECT
        routine_name AS name,
        routine_schema AS schema,
        routine_type AS type,
        data_type AS return_type
      FROM information_schema.routines
      WHERE routine_schema = $1
        AND routine_type IN ('PROCEDURE', 'FUNCTION')
        AND data_type != 'trigger'
      ORDER BY routine_type, routine_name
    `;
    const rows = await db.query(query, [targetSchema]);
    return rows.map((r: any) => ({
      name: r.name,
      schema: r.schema,
      type: r.type as "PROCEDURE" | "FUNCTION",
      returnType: r.return_type || undefined,
    }));
  }

  private async listExternalProcedures(
    connectionId: number,
    schema?: string
  ): Promise<ProcedureListItem[]> {
    const poolInfo = await getExternalPool(connectionId);
    const dbType = poolInfo.dbType.toLowerCase();

    let query: string;
    let params: any[];

    switch (dbType) {
      case "postgresql": {
        const targetSchema = schema || "public";
        query = `
          SELECT
            routine_name AS name,
            routine_schema AS schema,
            routine_type AS type,
            data_type AS return_type
          FROM information_schema.routines
          WHERE routine_schema = $1
            AND routine_type IN ('PROCEDURE', 'FUNCTION')
            AND data_type != 'trigger'
          ORDER BY routine_type, routine_name
        `;
        params = [targetSchema];
        break;
      }
      case "mysql":
      case "mariadb": {
        query = `
          SELECT
            ROUTINE_NAME AS name,
            ROUTINE_SCHEMA AS \`schema\`,
            ROUTINE_TYPE AS type,
            DATA_TYPE AS return_type
          FROM information_schema.ROUTINES
          WHERE ROUTINE_SCHEMA = DATABASE()
            AND ROUTINE_TYPE IN ('PROCEDURE', 'FUNCTION')
          ORDER BY ROUTINE_TYPE, ROUTINE_NAME
        `;
        params = [];
        break;
      }
      case "mssql": {
        query = `
          SELECT
            ROUTINE_NAME AS name,
            ROUTINE_SCHEMA AS [schema],
            ROUTINE_TYPE AS type,
            DATA_TYPE AS return_type
          FROM INFORMATION_SCHEMA.ROUTINES
          WHERE ROUTINE_TYPE IN ('PROCEDURE', 'FUNCTION')
          ORDER BY ROUTINE_TYPE, ROUTINE_NAME
        `;
        params = [];
        break;
      }
      default:
        throw new Error(`프로시저 목록 조회 미지원 DB: ${dbType}`);
    }

    const result = await executeExternalQuery(connectionId, query, params);
    return (result.rows || []).map((r: any) => ({
      name: r.name || r.NAME,
      schema: r.schema || r.SCHEMA || "",
      type: (r.type || r.TYPE || "FUNCTION").toUpperCase() as "PROCEDURE" | "FUNCTION",
      returnType: r.return_type || r.RETURN_TYPE || undefined,
    }));
  }

  /**
   * 프로시저/함수 파라미터 정보 조회
   */
  async getProcedureParameters(
    procedureName: string,
    dbSource: "internal" | "external",
    connectionId?: number,
    schema?: string
  ): Promise<ProcedureParameterInfo[]> {
    if (dbSource === "external" && connectionId) {
      return this.getExternalProcedureParameters(
        connectionId,
        procedureName,
        schema
      );
    }
    return this.getInternalProcedureParameters(procedureName, schema);
  }

  private async getInternalProcedureParameters(
    procedureName: string,
    schema?: string
  ): Promise<ProcedureParameterInfo[]> {
    const targetSchema = schema || "public";
    // PostgreSQL의 specific_name은 routine_name + OID 형태이므로 서브쿼리로 매칭
    const query = `
      SELECT
        p.parameter_name AS name,
        p.ordinal_position AS position,
        p.data_type,
        p.parameter_mode AS mode,
        p.parameter_default AS default_value
      FROM information_schema.parameters p
      WHERE p.specific_schema = $1
        AND p.specific_name IN (
          SELECT r.specific_name FROM information_schema.routines r
          WHERE r.routine_schema = $1 AND r.routine_name = $2
          LIMIT 1
        )
        AND p.parameter_name IS NOT NULL
      ORDER BY p.ordinal_position
    `;
    const rows = await db.query(query, [targetSchema, procedureName]);
    return rows.map((r: any) => ({
      name: r.name,
      position: parseInt(r.position, 10),
      dataType: r.data_type,
      mode: this.normalizeParamMode(r.mode),
      defaultValue: r.default_value || undefined,
    }));
  }

  private async getExternalProcedureParameters(
    connectionId: number,
    procedureName: string,
    schema?: string
  ): Promise<ProcedureParameterInfo[]> {
    const poolInfo = await getExternalPool(connectionId);
    const dbType = poolInfo.dbType.toLowerCase();

    let query: string;
    let params: any[];

    switch (dbType) {
      case "postgresql": {
        const targetSchema = schema || "public";
        query = `
          SELECT
            p.parameter_name AS name,
            p.ordinal_position AS position,
            p.data_type,
            p.parameter_mode AS mode,
            p.parameter_default AS default_value
          FROM information_schema.parameters p
          WHERE p.specific_schema = $1
            AND p.specific_name IN (
              SELECT r.specific_name FROM information_schema.routines r
              WHERE r.routine_schema = $1 AND r.routine_name = $2
              LIMIT 1
            )
            AND p.parameter_name IS NOT NULL
          ORDER BY p.ordinal_position
        `;
        params = [targetSchema, procedureName];
        break;
      }
      case "mysql":
      case "mariadb": {
        query = `
          SELECT
            PARAMETER_NAME AS name,
            ORDINAL_POSITION AS position,
            DATA_TYPE AS data_type,
            PARAMETER_MODE AS mode,
            '' AS default_value
          FROM information_schema.PARAMETERS
          WHERE SPECIFIC_SCHEMA = DATABASE()
            AND SPECIFIC_NAME = ?
            AND PARAMETER_NAME IS NOT NULL
          ORDER BY ORDINAL_POSITION
        `;
        params = [procedureName];
        break;
      }
      case "mssql": {
        query = `
          SELECT
            PARAMETER_NAME AS name,
            ORDINAL_POSITION AS position,
            DATA_TYPE AS data_type,
            PARAMETER_MODE AS mode,
            '' AS default_value
          FROM INFORMATION_SCHEMA.PARAMETERS
          WHERE SPECIFIC_NAME = @p1
            AND PARAMETER_NAME IS NOT NULL
          ORDER BY ORDINAL_POSITION
        `;
        params = [procedureName];
        break;
      }
      default:
        throw new Error(`파라미터 조회 미지원 DB: ${dbType}`);
    }

    const result = await executeExternalQuery(connectionId, query, params);
    return (result.rows || []).map((r: any) => ({
      name: (r.name || r.NAME || "").replace(/^@/, ""),
      position: parseInt(r.position || r.POSITION || "0", 10),
      dataType: r.data_type || r.DATA_TYPE || "unknown",
      mode: this.normalizeParamMode(r.mode || r.MODE),
      defaultValue: r.default_value || r.DEFAULT_VALUE || undefined,
    }));
  }

  /**
   * 프로시저/함수 실행
   * 내부 DB는 기존 트랜잭션 client를 사용, 외부 DB는 별도 연결
   */
  async executeProcedure(
    config: FlowProcedureConfig,
    recordData: Record<string, any>,
    client?: any
  ): Promise<{ success: boolean; result?: any; error?: string }> {
    const paramValues = this.resolveParameters(config.parameters, recordData);

    if (config.dbSource === "internal") {
      return this.executeInternalProcedure(config, paramValues, client);
    }

    if (!config.connectionId) {
      throw new Error("외부 DB 프로시저 호출에 connectionId가 필요합니다");
    }
    return this.executeExternalProcedure(config, paramValues);
  }

  /**
   * 내부 DB 프로시저 실행 (트랜잭션 client 공유)
   */
  private async executeInternalProcedure(
    config: FlowProcedureConfig,
    paramValues: any[],
    client?: any
  ): Promise<{ success: boolean; result?: any; error?: string }> {
    const schema = config.procedureSchema || "public";
    const safeName = this.sanitizeName(config.procedureName);
    const safeSchema = this.sanitizeName(schema);
    const qualifiedName = `${safeSchema}.${safeName}`;

    const placeholders = paramValues.map((_, i) => `$${i + 1}`).join(", ");

    let sql: string;
    if (config.callType === "function") {
      // SELECT * FROM fn()을 사용하여 OUT 파라미터를 개별 컬럼으로 반환
      sql = `SELECT * FROM ${qualifiedName}(${placeholders})`;
    } else {
      sql = `CALL ${qualifiedName}(${placeholders})`;
    }

    try {
      const executor = client || db;
      const result = client
        ? await client.query(sql, paramValues)
        : await db.query(sql, paramValues);

      const rows = client ? result.rows : result;
      return { success: true, result: rows };
    } catch (error: any) {
      throw new Error(
        `프로시저 실행 실패 [${qualifiedName}]: ${error.message}`
      );
    }
  }

  /**
   * 외부 DB 프로시저 실행
   */
  private async executeExternalProcedure(
    config: FlowProcedureConfig,
    paramValues: any[]
  ): Promise<{ success: boolean; result?: any; error?: string }> {
    const connectionId = config.connectionId!;
    const poolInfo = await getExternalPool(connectionId);
    const dbType = poolInfo.dbType.toLowerCase();
    const safeName = this.sanitizeName(config.procedureName);
    const safeSchema = config.procedureSchema
      ? this.sanitizeName(config.procedureSchema)
      : null;

    let sql: string;

    switch (dbType) {
      case "postgresql": {
        const qualifiedName = safeSchema
          ? `${safeSchema}.${safeName}`
          : safeName;
        const placeholders = paramValues.map((_, i) => `$${i + 1}`).join(", ");
        sql =
          config.callType === "function"
            ? `SELECT * FROM ${qualifiedName}(${placeholders})`
            : `CALL ${qualifiedName}(${placeholders})`;
        break;
      }
      case "mysql":
      case "mariadb": {
        const placeholders = paramValues.map(() => "?").join(", ");
        sql = `CALL ${safeName}(${placeholders})`;
        break;
      }
      case "mssql": {
        const paramList = paramValues
          .map((_, i) => `@p${i + 1}`)
          .join(", ");
        sql = `EXEC ${safeName} ${paramList}`;
        break;
      }
      default:
        throw new Error(`프로시저 실행 미지원 DB: ${dbType}`);
    }

    try {
      const result = await executeExternalQuery(connectionId, sql, paramValues);
      return { success: true, result: result.rows };
    } catch (error: any) {
      throw new Error(
        `외부 프로시저 실행 실패 [${safeName}]: ${error.message}`
      );
    }
  }

  /**
   * 설정된 파라미터 매핑에서 실제 값을 추출
   */
  private resolveParameters(
    params: FlowProcedureParam[],
    recordData: Record<string, any>
  ): any[] {
    const inParams = params.filter((p) => p.mode === "IN" || p.mode === "INOUT");
    return inParams.map((param) => {
      switch (param.source) {
        case "record_field":
          if (!param.field) {
            throw new Error(`파라미터 ${param.name}: 레코드 필드가 지정되지 않았습니다`);
          }
          return recordData[param.field] ?? null;

        case "static":
          return param.value ?? null;

        case "step_variable":
          return recordData[param.field || param.name] ?? param.value ?? null;

        default:
          return null;
      }
    });
  }

  /**
   * 이름(스키마/프로시저) SQL Injection 방지용 검증
   */
  private sanitizeName(name: string): string {
    if (!/^[a-zA-Z0-9_]+$/.test(name)) {
      throw new Error(`유효하지 않은 이름: ${name}`);
    }
    return name;
  }

  /**
   * 파라미터 모드 정규화
   */
  private normalizeParamMode(mode: string | null): "IN" | "OUT" | "INOUT" {
    if (!mode) return "IN";
    const upper = mode.toUpperCase();
    if (upper === "OUT") return "OUT";
    if (upper === "INOUT") return "INOUT";
    return "IN";
  }
}
