import { v4 as uuidv4 } from "uuid";
import { query, queryOne, transaction } from "../database/db";
import {
  ReportMaster,
  ReportLayout,
  ReportQuery,
  ReportTemplate,
  ReportDetail,
  GetReportsParams,
  GetReportsResponse,
  CreateReportRequest,
  UpdateReportRequest,
  SaveLayoutRequest,
  GetTemplatesResponse,
  CreateTemplateRequest,
  VisualQuery,
} from "../types/report";
import { DatabaseConnectorFactory } from "../database/DatabaseConnectorFactory";
import { ExternalDbConnectionService } from "./externalDbConnectionService";
import { logger } from "../utils/logger";

const REPORT_TYPE_LABELS: Record<string, string> = {
  ORDER: "발주서",
  INVOICE: "청구서",
  STATEMENT: "거래명세서",
  RECEIPT: "영수증",
  BASIC: "기본",
};

const ALLOWED_SORT_COLUMNS = [
  "created_at",
  "updated_at",
  "report_name_kor",
  "report_name_eng",
  "report_type",
  "use_yn",
] as const;

const ALLOWED_SORT_ORDERS = ["ASC", "DESC"] as const;

const DEFAULT_MARGINS = { top: 20, bottom: 20, left: 20, right: 20 };
const DEFAULT_CANVAS_WIDTH = 210;
const DEFAULT_CANVAS_HEIGHT = 297;

function generateReportId(): string {
  return `RPT_${uuidv4().replace(/-/g, "").substring(0, 20)}`;
}

function generateLayoutId(): string {
  return `LAY_${uuidv4().replace(/-/g, "").substring(0, 20)}`;
}

function generateQueryId(): string {
  return `QRY_${uuidv4().replace(/-/g, "").substring(0, 20)}`;
}

function generateTemplateId(): string {
  return `TPL_${uuidv4().replace(/-/g, "").substring(0, 20)}`;
}

function findTypeCodesByLabel(searchText: string): string[] {
  const lower = searchText.toLowerCase();
  return Object.entries(REPORT_TYPE_LABELS)
    .filter(([code, label]) => label.includes(searchText) || code.toLowerCase().includes(lower))
    .map(([code]) => code);
}

function parseJsonComponents(raw: string | Record<string, unknown> | null): Record<string, unknown> | null {
  if (raw === null || raw === undefined) return null;
  if (typeof raw === "string") {
    try {
      return JSON.parse(raw);
    } catch {
      return null;
    }
  }
  return raw;
}

function sanitizeSortBy(sortBy: string): string {
  if ((ALLOWED_SORT_COLUMNS as readonly string[]).includes(sortBy)) {
    return sortBy;
  }
  return "created_at";
}

function sanitizeSortOrder(sortOrder: string): "ASC" | "DESC" {
  const upper = sortOrder.toUpperCase();
  if ((ALLOWED_SORT_ORDERS as readonly string[]).includes(upper)) {
    return upper as "ASC" | "DESC";
  }
  return "DESC";
}

export class ReportService {
  private validateQuerySafety(sql: string): void {
    const dangerousKeywords = [
      "DELETE",
      "DROP",
      "TRUNCATE",
      "INSERT",
      "UPDATE",
      "ALTER",
      "CREATE",
      "REPLACE",
      "MERGE",
      "GRANT",
      "REVOKE",
      "EXECUTE",
      "EXEC",
      "CALL",
    ];

    const upperSql = sql.toUpperCase().trim();

    for (const keyword of dangerousKeywords) {
      const regex = new RegExp(`\\b${keyword}\\b`, "i");
      if (regex.test(upperSql)) {
        throw new Error(
          `보안상의 이유로 ${keyword} 명령어는 사용할 수 없습니다. SELECT 쿼리만 허용됩니다.`
        );
      }
    }

    if (!upperSql.startsWith("SELECT") && !upperSql.startsWith("WITH")) {
      throw new Error(
        "SELECT 쿼리만 허용됩니다. 데이터 조회 용도로만 사용할 수 있습니다."
      );
    }

    const semicolonCount = (sql.match(/;/g) || []).length;
    if (
      semicolonCount > 1 ||
      (semicolonCount === 1 && !sql.trim().endsWith(";"))
    ) {
      throw new Error(
        "보안상의 이유로 여러 개의 쿼리를 동시에 실행할 수 없습니다."
      );
    }
  }

  async getReports(params: GetReportsParams, companyCode: string): Promise<GetReportsResponse> {
    const {
      page = 1,
      limit = 20,
      searchText = "",
      searchField,
      startDate,
      endDate,
      reportType = "",
      useYn = "Y",
      sortBy = "created_at",
      sortOrder = "DESC",
    } = params;

    const offset = (page - 1) * limit;
    const safeSortBy = sanitizeSortBy(sortBy);
    const safeSortOrder = sanitizeSortOrder(sortOrder);

    const conditions: string[] = [];
    const values: (string | number)[] = [];
    let paramIndex = 1;

    this.applyCompanyCodeFilter(conditions, values, paramIndex, companyCode, "rm");
    paramIndex = values.length + 1;

    if (useYn) {
      conditions.push(`rm.use_yn = $${paramIndex++}`);
      values.push(useYn);
    }

    paramIndex = this.applySearchConditions(
      conditions, values, paramIndex, searchText, searchField, startDate, endDate
    );

    if (reportType) {
      conditions.push(`rm.report_type = $${paramIndex++}`);
      values.push(reportType);
    }

    const whereClause =
      conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";

    const countResult = await queryOne<{ total: string }>(
      `SELECT COUNT(*) as total FROM report_master rm ${whereClause}`,
      values
    );
    const total = parseInt(countResult?.total || "0", 10);

    const listQuery = `
      SELECT 
        rm.report_id, rm.report_name_kor, rm.report_name_eng,
        rm.template_id, rt.template_name_kor AS template_name,
        rm.report_type, rm.company_code, rm.description, rm.use_yn,
        rm.created_at, rm.created_by, rm.updated_at, rm.updated_by
      FROM report_master rm
      LEFT JOIN report_template rt ON rm.template_id = rt.template_id
      ${whereClause}
      ORDER BY rm.${safeSortBy} ${safeSortOrder}
      LIMIT $${paramIndex++} OFFSET $${paramIndex}
    `;

    const items = await query<ReportMaster>(listQuery, [...values, limit, offset]);

    const { typeSummary, allTypes, recentActivity, recentTotal } =
      await this.getReportStatistics(companyCode);

    return {
      items, total, page, limit,
      typeSummary, allTypes, recentActivity, recentTotal,
    };
  }

  private applyCompanyCodeFilter(
    conditions: string[],
    values: (string | number)[],
    paramIndex: number,
    companyCode: string,
    alias: string
  ): void {
    if (companyCode !== "*") {
      conditions.push(`${alias}.company_code = $${paramIndex}`);
      values.push(companyCode);
    }
  }

  private applySearchConditions(
    conditions: string[],
    values: (string | number)[],
    paramIndex: number,
    searchText: string,
    searchField?: string,
    startDate?: string,
    endDate?: string
  ): number {
    const isDateRangeSearch =
      (searchField === "created_at" || searchField === "updated_at") && startDate && endDate;

    if (isDateRangeSearch) {
      const dateColumn = searchField === "created_at"
        ? "rm.created_at"
        : "COALESCE(rm.updated_at, rm.created_at)";
      conditions.push(`${dateColumn} >= $${paramIndex}::date`);
      values.push(startDate!);
      paramIndex++;
      conditions.push(`${dateColumn} < ($${paramIndex}::date + INTERVAL '1 day')`);
      values.push(endDate!);
      paramIndex++;
    } else if (searchText) {
      paramIndex = this.applyTextSearch(conditions, values, paramIndex, searchText, searchField);
    }

    return paramIndex;
  }

  private applyTextSearch(
    conditions: string[],
    values: (string | number)[],
    paramIndex: number,
    searchText: string,
    searchField?: string
  ): number {
    if (searchField === "created_by") {
      conditions.push(`rm.created_by LIKE $${paramIndex}`);
      values.push(`%${searchText}%`);
      paramIndex++;
    } else if (searchField === "report_type") {
      const matchedCodes = findTypeCodesByLabel(searchText);
      if (matchedCodes.length > 0) {
        const placeholders = matchedCodes.map(() => `$${paramIndex++}`).join(", ");
        conditions.push(`rm.report_type IN (${placeholders})`);
        values.push(...matchedCodes);
      } else {
        conditions.push(`rm.report_type LIKE $${paramIndex}`);
        values.push(`%${searchText}%`);
        paramIndex++;
      }
    } else if (searchField === "updated_at") {
      conditions.push(`CAST(rm.updated_at AS TEXT) LIKE $${paramIndex}`);
      values.push(`%${searchText}%`);
      paramIndex++;
    } else {
      conditions.push(
        `(rm.report_name_kor LIKE $${paramIndex} OR rm.report_name_eng LIKE $${paramIndex})`
      );
      values.push(`%${searchText}%`);
      paramIndex++;
    }
    return paramIndex;
  }

  private async getReportStatistics(companyCode: string) {
    const companyFilter = companyCode !== "*" ? " AND company_code = $1" : "";
    const companyParams = companyCode !== "*" ? [companyCode] : [];

    const typeSummaryRows = await query<{ report_type: string; count: string }>(
      `SELECT report_type, COUNT(*) as count
       FROM report_master
       WHERE use_yn = 'Y' AND report_type IS NOT NULL AND report_type != ''${companyFilter}
       GROUP BY report_type
       ORDER BY count DESC`,
      companyParams
    );
    const typeSummary = typeSummaryRows.map((r) => ({
      type: r.report_type,
      count: parseInt(r.count, 10),
    }));
    const allTypes = typeSummary.map((t) => t.type).sort();

    const recentActivityRows = await query<{ date_label: string; date_raw: string; count: string }>(
      `SELECT TO_CHAR(COALESCE(updated_at, created_at), 'MM/DD') AS date_label,
              MAX(COALESCE(updated_at, created_at)) AS date_raw,
              COUNT(*) AS count
       FROM report_master
       WHERE use_yn = 'Y'
         AND COALESCE(updated_at, created_at) >= NOW() - INTERVAL '30 days'${companyFilter}
       GROUP BY date_label
       ORDER BY count DESC, date_raw DESC
       LIMIT 3`,
      companyParams
    );
    const recentActivity = recentActivityRows
      .map((r) => ({ date: r.date_label, count: parseInt(r.count, 10) }))
      .sort((a, b) => a.count - b.count);

    const recentCountResult = await queryOne<{ count: string }>(
      `SELECT COUNT(*) AS count FROM report_master
       WHERE use_yn = 'Y'
         AND COALESCE(updated_at, created_at) >= NOW() - INTERVAL '30 days'${companyFilter}`,
      companyParams
    );
    const recentTotal = parseInt(recentCountResult?.count || "0", 10);

    return { typeSummary, allTypes, recentActivity, recentTotal };
  }

  async getReportById(reportId: string, companyCode: string): Promise<ReportDetail | null> {
    const companyCondition = companyCode !== "*" ? " AND company_code = $2" : "";
    const reportParams = companyCode !== "*" ? [reportId, companyCode] : [reportId];

    const report = await queryOne<ReportMaster>(
      `SELECT report_id, report_name_kor, report_name_eng, template_id,
              report_type, company_code, description, use_yn,
              created_at, created_by, updated_at, updated_by
       FROM report_master
       WHERE report_id = $1${companyCondition}`,
      reportParams
    );

    if (!report) return null;

    const layout = await this.getLayoutInternal(reportId);

    const queries = await query<ReportQuery>(
      `SELECT query_id, report_id, query_name, query_type, sql_query,
              parameters, external_connection_id, display_order,
              created_at, created_by, updated_at, updated_by
       FROM report_query
       WHERE report_id = $1
       ORDER BY display_order, created_at`,
      [reportId]
    );

    const menuMappings = await query<{ menu_objid: number }>(
      `SELECT menu_objid FROM report_menu_mapping WHERE report_id = $1 ORDER BY created_at`,
      [reportId]
    );
    const menuObjids = menuMappings?.map((m) => Number(m.menu_objid)) || [];

    return { report, layout, queries: queries || [], menuObjids };
  }

  async createReport(
    data: CreateReportRequest,
    userId: string
  ): Promise<string> {
    const reportId = generateReportId();

    return transaction(async (client) => {
      await client.query(
        `INSERT INTO report_master (
          report_id, report_name_kor, report_name_eng, template_id,
          report_type, company_code, description, use_yn, created_by
        ) VALUES ($1, $2, $3, $4, $5, $6, $7, 'Y', $8)`,
        [
          reportId, data.reportNameKor, data.reportNameEng || null,
          data.templateId || null, data.reportType,
          data.companyCode || null, data.description || null, userId,
        ]
      );

      if (data.templateId) {
        await this.createLayoutFromTemplate(client, data.templateId, reportId, userId);
      }

      return reportId;
    });
  }

  private async createLayoutFromTemplate(
    client: { query: (sql: string, params: unknown[]) => Promise<{ rows: Record<string, unknown>[] }> },
    templateId: string,
    reportId: string,
    userId: string
  ): Promise<void> {
    const template = await client.query(
      `SELECT layout_config FROM report_template WHERE template_id = $1`,
      [templateId]
    );

    if (template.rows.length === 0 || !template.rows[0].layout_config) return;

    const layoutConfig = JSON.parse(template.rows[0].layout_config as string);

    await client.query(
      `INSERT INTO report_layout (
        layout_id, report_id, canvas_width, canvas_height, page_orientation,
        margin_top, margin_bottom, margin_left, margin_right, components, created_by
      ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)`,
      [
        generateLayoutId(), reportId,
        layoutConfig.width || DEFAULT_CANVAS_WIDTH,
        layoutConfig.height || DEFAULT_CANVAS_HEIGHT,
        layoutConfig.orientation || "portrait",
        DEFAULT_MARGINS.top, DEFAULT_MARGINS.bottom,
        DEFAULT_MARGINS.left, DEFAULT_MARGINS.right,
        JSON.stringify([]), userId,
      ]
    );
  }

  async updateReport(
    reportId: string,
    data: UpdateReportRequest,
    userId: string,
    companyCode: string
  ): Promise<boolean> {
    const setClauses: string[] = [];
    const values: (string | number | null)[] = [];
    let paramIndex = 1;

    const fieldMap: Array<[keyof UpdateReportRequest, string]> = [
      ["reportNameKor", "report_name_kor"],
      ["reportNameEng", "report_name_eng"],
      ["reportType", "report_type"],
      ["description", "description"],
      ["useYn", "use_yn"],
    ];

    for (const [key, column] of fieldMap) {
      if (data[key] !== undefined) {
        setClauses.push(`${column} = $${paramIndex++}`);
        values.push(data[key] as string);
      }
    }

    if (setClauses.length === 0) return false;

    setClauses.push(`updated_at = CURRENT_TIMESTAMP`);
    setClauses.push(`updated_by = $${paramIndex++}`);
    values.push(userId);

    values.push(reportId);
    let whereClause = `WHERE report_id = $${paramIndex}`;

    if (companyCode !== "*") {
      paramIndex++;
      values.push(companyCode);
      whereClause += ` AND company_code = $${paramIndex}`;
    }

    await query(`UPDATE report_master SET ${setClauses.join(", ")} ${whereClause}`, values);
    return true;
  }

  async deleteReport(reportId: string, companyCode: string): Promise<boolean> {
    return transaction(async (client) => {
      const companyCondition = companyCode !== "*" ? " AND company_code = $2" : "";
      const params = companyCode !== "*" ? [reportId, companyCode] : [reportId];

      const existing = await client.query(
        `SELECT report_id FROM report_master WHERE report_id = $1${companyCondition}`,
        params
      );
      if (existing.rows.length === 0) return false;

      await client.query(`DELETE FROM report_menu_mapping WHERE report_id = $1`, [reportId]);
      await client.query(`DELETE FROM report_query WHERE report_id = $1`, [reportId]);
      await client.query(`DELETE FROM report_layout WHERE report_id = $1`, [reportId]);

      const result = await client.query(
        `DELETE FROM report_master WHERE report_id = $1${companyCondition}`,
        params
      );

      return (result.rowCount ?? 0) > 0;
    });
  }

  async copyReport(
    reportId: string,
    userId: string,
    companyCode: string,
    newName?: string
  ): Promise<string | null> {
    return transaction(async (client) => {
      const companyCondition = companyCode !== "*" ? " AND company_code = $2" : "";
      const params = companyCode !== "*" ? [reportId, companyCode] : [reportId];

      const originalResult = await client.query(
        `SELECT * FROM report_master WHERE report_id = $1${companyCondition}`,
        params
      );

      if (originalResult.rows.length === 0) return null;

      const original = originalResult.rows[0];
      const newReportId = generateReportId();

      await client.query(
        `INSERT INTO report_master (
          report_id, report_name_kor, report_name_eng, template_id,
          report_type, company_code, description, use_yn, created_by
        ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`,
        [
          newReportId,
          newName || `${original.report_name_kor} (복사)`,
          original.report_name_eng ? `${original.report_name_eng} (Copy)` : null,
          original.template_id,
          original.report_type,
          original.company_code,
          original.description,
          original.use_yn,
          userId,
        ]
      );

      await this.copyLayoutData(client, reportId, newReportId, userId);
      await this.copyQueryData(client, reportId, newReportId, userId);

      return newReportId;
    });
  }

  private async copyLayoutData(
    client: { query: (sql: string, params: unknown[]) => Promise<{ rows: Record<string, unknown>[] }> },
    sourceReportId: string,
    targetReportId: string,
    userId: string
  ): Promise<void> {
    const layoutResult = await client.query(
      `SELECT * FROM report_layout WHERE report_id = $1`,
      [sourceReportId]
    );

    if (layoutResult.rows.length === 0) return;

    const originalLayout = layoutResult.rows[0];
    const componentsData = typeof originalLayout.components === "string"
      ? originalLayout.components
      : JSON.stringify(originalLayout.components);

    await client.query(
      `INSERT INTO report_layout (
        layout_id, report_id, canvas_width, canvas_height, page_orientation,
        margin_top, margin_bottom, margin_left, margin_right, components, created_by
      ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)`,
      [
        generateLayoutId(), targetReportId,
        originalLayout.canvas_width, originalLayout.canvas_height,
        originalLayout.page_orientation,
        originalLayout.margin_top, originalLayout.margin_bottom,
        originalLayout.margin_left, originalLayout.margin_right,
        componentsData, userId,
      ]
    );
  }

  private async copyQueryData(
    client: { query: (sql: string, params: unknown[]) => Promise<{ rows: Record<string, unknown>[] }> },
    sourceReportId: string,
    targetReportId: string,
    userId: string
  ): Promise<void> {
    const queriesResult = await client.query(
      `SELECT * FROM report_query WHERE report_id = $1 ORDER BY display_order`,
      [sourceReportId]
    );

    for (const originalQuery of queriesResult.rows) {
      await client.query(
        `INSERT INTO report_query (
          query_id, report_id, query_name, query_type, sql_query,
          parameters, external_connection_id, display_order, created_by
        ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`,
        [
          generateQueryId(), targetReportId,
          originalQuery.query_name, originalQuery.query_type,
          originalQuery.sql_query, JSON.stringify(originalQuery.parameters),
          originalQuery.external_connection_id || null,
          originalQuery.display_order, userId,
        ]
      );
    }
  }

  async getLayout(reportId: string, companyCode?: string): Promise<ReportLayout | null> {
    if (companyCode && companyCode !== "*") {
      const ownerCheck = await queryOne<{ report_id: string }>(
        `SELECT report_id FROM report_master WHERE report_id = $1 AND company_code = $2`,
        [reportId, companyCode]
      );
      if (!ownerCheck) return null;
    }
    return this.getLayoutInternal(reportId);
  }

  private async getLayoutInternal(reportId: string): Promise<ReportLayout | null> {
    const layoutRaw = await queryOne<ReportLayout>(
      `SELECT layout_id, report_id, canvas_width, canvas_height,
              page_orientation, margin_top, margin_bottom, margin_left, margin_right,
              components, created_at, created_by, updated_at, updated_by
       FROM report_layout
       WHERE report_id = $1`,
      [reportId]
    );
    if (!layoutRaw) return null;

    return {
      ...layoutRaw,
      components: parseJsonComponents(layoutRaw.components as string | Record<string, unknown> | null) as unknown as string,
    };
  }

  async saveLayout(
    reportId: string,
    data: SaveLayoutRequest,
    userId: string,
    companyCode: string
  ): Promise<boolean> {
    return transaction(async (client) => {
      if (companyCode !== "*") {
        const ownerCheck = await client.query(
          `SELECT report_id FROM report_master WHERE report_id = $1 AND company_code = $2`,
          [reportId, companyCode]
        );
        if (ownerCheck.rows.length === 0) return false;
      }

      const firstPage = data.layoutConfig.pages[0];
      const canvasWidth = firstPage?.width || DEFAULT_CANVAS_WIDTH;
      const canvasHeight = firstPage?.height || DEFAULT_CANVAS_HEIGHT;
      const pageOrientation = canvasWidth > canvasHeight ? "landscape" : "portrait";
      const margins = firstPage?.margins || DEFAULT_MARGINS;

      await this.upsertLayout(client, reportId, {
        canvasWidth, canvasHeight, pageOrientation, margins,
        componentsJson: JSON.stringify(data.layoutConfig),
        userId,
      });

      if (data.queries && data.queries.length > 0) {
        await this.replaceQueries(client, reportId, data.queries, userId);
      }

      if (data.menuObjids !== undefined) {
        await this.replaceMenuMappings(client, reportId, data.menuObjids, companyCode, userId);
      }

      await client.query(
        `UPDATE report_master SET updated_at = CURRENT_TIMESTAMP, updated_by = $1 WHERE report_id = $2`,
        [userId, reportId]
      );

      return true;
    });
  }

  private async upsertLayout(
    client: { query: (sql: string, params: unknown[]) => Promise<{ rows: Record<string, unknown>[] }> },
    reportId: string,
    opts: {
      canvasWidth: number; canvasHeight: number; pageOrientation: string;
      margins: { top: number; bottom: number; left: number; right: number };
      componentsJson: string; userId: string;
    }
  ): Promise<void> {
    const existing = await client.query(
      `SELECT layout_id FROM report_layout WHERE report_id = $1`,
      [reportId]
    );

    const layoutParams = [
      opts.canvasWidth, opts.canvasHeight, opts.pageOrientation,
      opts.margins.top, opts.margins.bottom, opts.margins.left, opts.margins.right,
      opts.componentsJson, opts.userId,
    ];

    if (existing.rows.length > 0) {
      await client.query(
        `UPDATE report_layout SET
          canvas_width = $1, canvas_height = $2, page_orientation = $3,
          margin_top = $4, margin_bottom = $5, margin_left = $6, margin_right = $7,
          components = $8, updated_at = CURRENT_TIMESTAMP, updated_by = $9
         WHERE report_id = $10`,
        [...layoutParams, reportId]
      );
    } else {
      await client.query(
        `INSERT INTO report_layout (
          layout_id, report_id, canvas_width, canvas_height, page_orientation,
          margin_top, margin_bottom, margin_left, margin_right, components, created_by
        ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)`,
        [generateLayoutId(), reportId, ...layoutParams]
      );
    }
  }

  private async replaceQueries(
    client: { query: (sql: string, params: unknown[]) => Promise<{ rows: Record<string, unknown>[] }> },
    reportId: string,
    queries: NonNullable<SaveLayoutRequest["queries"]>,
    userId: string
  ): Promise<void> {
    await client.query(`DELETE FROM report_query WHERE report_id = $1`, [reportId]);

    for (let i = 0; i < queries.length; i++) {
      const q = queries[i];
      await client.query(
        `INSERT INTO report_query (
          query_id, report_id, query_name, query_type, sql_query,
          parameters, external_connection_id, display_order, created_by
        ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`,
        [
          q.id, reportId, q.name, q.type, q.sqlQuery,
          JSON.stringify(q.parameters),
          q.externalConnectionId || null,
          i, userId,
        ]
      );
    }
  }

  private async replaceMenuMappings(
    client: { query: (sql: string, params: unknown[]) => Promise<{ rows: Record<string, unknown>[] }> },
    reportId: string,
    menuObjids: number[],
    companyCode: string,
    userId: string
  ): Promise<void> {
    await client.query(`DELETE FROM report_menu_mapping WHERE report_id = $1`, [reportId]);

    if (menuObjids.length === 0) return;

    const reportResult = await client.query(
      `SELECT company_code FROM report_master WHERE report_id = $1`,
      [reportId]
    );
    const resolvedCompanyCode = (reportResult.rows[0]?.company_code as string) || companyCode;

    for (const menuObjid of menuObjids) {
      await client.query(
        `INSERT INTO report_menu_mapping (report_id, menu_objid, company_code, created_by)
         VALUES ($1, $2, $3, $4)`,
        [reportId, menuObjid, resolvedCompanyCode, userId]
      );
    }
  }

  async executeQuery(
    reportId: string,
    queryId: string,
    parameters: Record<string, string | number | null>,
    sqlQuery?: string,
    externalConnectionId?: number | null
  ): Promise<{ fields: string[]; rows: Record<string, unknown>[] }> {
    let sqlToExecute: string;
    let queryParameters: string[] = [];
    let connectionId: number | null = externalConnectionId ?? null;

    if (sqlQuery) {
      sqlToExecute = sqlQuery;
      queryParameters = this.extractUniqueParams(sqlQuery);
    } else {
      const storedQuery = await queryOne<ReportQuery>(
        `SELECT * FROM report_query WHERE query_id = $1 AND report_id = $2`,
        [queryId, reportId]
      );

      if (!storedQuery) {
        throw new Error("쿼리를 찾을 수 없습니다.");
      }

      sqlToExecute = storedQuery.sql_query;
      queryParameters = Array.isArray(storedQuery.parameters) ? storedQuery.parameters : [];
      connectionId = storedQuery.external_connection_id;
    }

    this.validateQuerySafety(sqlToExecute);

    const paramArray = queryParameters.map((param) => parameters[param] ?? null);
    const { sql: finalSql, params: finalParams } =
      this.buildPreviewSqlIfNeeded(sqlToExecute, queryParameters, paramArray);

    try {
      const result = connectionId
        ? await this.executeOnExternalDb(finalSql, connectionId)
        : await query(finalSql, finalParams);

      const fields = result.length > 0 ? Object.keys(result[0]) : [];
      return { fields, rows: result as Record<string, unknown>[] };
    } catch (error: unknown) {
      const message = error instanceof Error ? error.message : "알 수 없는 오류";
      throw new Error(`쿼리 실행 오류: ${message}`);
    }
  }

  private extractUniqueParams(sql: string): string[] {
    const matches = sql.match(/\$\d+/g);
    if (!matches) return [];
    return [...new Set(matches)];
  }

  private buildPreviewSqlIfNeeded(
    sql: string,
    queryParameters: string[],
    paramArray: (string | number | null)[]
  ): { sql: string; params: (string | number | null)[] } {
    const allParamsNull = paramArray.length > 0 && paramArray.every((p) => p === null);
    if (!allParamsNull) return { sql, params: paramArray };

    let previewSql = sql;
    for (const param of queryParameters) {
      const escapedParam = param.replace("$", "\\$");
      const conditionPattern = new RegExp(
        `\\S+\\s*(?:=|!=|<>|>=|<=|>|<|LIKE|ILIKE|IN\\s*\\()\\s*${escapedParam}\\)?`,
        "gi"
      );
      previewSql = previewSql.replace(conditionPattern, "TRUE");
    }

    if (!/\bLIMIT\b/i.test(previewSql)) {
      previewSql = previewSql.replace(/;?\s*$/, " LIMIT 100");
    }

    return { sql: previewSql, params: [] };
  }

  private async executeOnExternalDb(
    sql: string,
    connectionId: number
  ): Promise<Record<string, unknown>[]> {
    const connectionResult = await ExternalDbConnectionService.getConnectionById(connectionId);

    if (!connectionResult.success || !connectionResult.data) {
      throw new Error("외부 DB 연결 정보를 찾을 수 없습니다.");
    }

    const connection = connectionResult.data;
    const connector = await DatabaseConnectorFactory.createConnector(
      connection.db_type,
      {
        host: connection.host,
        port: connection.port,
        database: connection.database_name,
        user: connection.username,
        password: connection.password,
        connectionTimeoutMillis: connection.connection_timeout || 30000,
        queryTimeoutMillis: connection.query_timeout || 30000,
      },
      connectionId
    );

    await connector.connect();
    try {
      const queryResult = await connector.executeQuery(sql);
      return (queryResult.rows || []) as Record<string, unknown>[];
    } finally {
      await connector.disconnect();
    }
  }

  async getReportsByMenuObjid(
    menuObjid: number,
    companyCode: string
  ): Promise<{ items: ReportMaster[]; total: number }> {
    // 매핑 없는 리포트(글로벌)는 어느 메뉴에서나 보이고,
    // 매핑 있는 리포트는 해당 menu_objid에 매핑된 경우에만 보임.
    const companyFilter = companyCode !== "*" ? " AND rm.company_code = $2" : "";
    const params = companyCode !== "*" ? [menuObjid, companyCode] : [menuObjid];

    const items = await query<ReportMaster>(
      `SELECT DISTINCT rm.report_id, rm.report_name_kor, rm.report_name_eng,
              rm.template_id, rt.template_name_kor AS template_name,
              rm.report_type, rm.company_code, rm.description, rm.use_yn,
              rm.created_at, rm.created_by, rm.updated_at, rm.updated_by
       FROM report_master rm
       LEFT JOIN report_template rt ON rm.template_id = rt.template_id
       WHERE rm.use_yn = 'Y'${companyFilter}
         AND (
           NOT EXISTS (SELECT 1 FROM report_menu_mapping WHERE report_id = rm.report_id)
           OR EXISTS (SELECT 1 FROM report_menu_mapping WHERE report_id = rm.report_id AND menu_objid = $1)
         )
       ORDER BY rm.report_name_kor ASC`,
      params
    );

    return { items: items || [], total: (items || []).length };
  }

  async getTemplates(): Promise<GetTemplatesResponse> {
    const templateQuery = `
      SELECT
        template_id,
        template_name_kor,
        template_name_eng,
        template_type,
        is_system,
        thumbnail_url,
        description,
        layout_config,
        default_queries,
        use_yn,
        sort_order,
        created_at,
        created_by,
        updated_at,
        updated_by
      FROM report_template
      WHERE use_yn = 'Y'
      ORDER BY is_system DESC, sort_order ASC
    `;

    const templates = await query<ReportTemplate>(templateQuery);

    const system = templates.filter((t) => t.is_system === "Y");
    const custom = templates.filter((t) => t.is_system === "N");

    return { system, custom };
  }

  async createTemplate(
    data: CreateTemplateRequest,
    userId: string
  ): Promise<string> {
    const templateId = generateTemplateId();

    const insertQuery = `
      INSERT INTO report_template (
        template_id,
        template_name_kor,
        template_name_eng,
        template_type,
        is_system,
        description,
        layout_config,
        default_queries,
        use_yn,
        created_by
      ) VALUES ($1, $2, $3, $4, 'N', $5, $6, $7, 'Y', $8)
    `;

    await query(insertQuery, [
      templateId,
      data.templateNameKor,
      data.templateNameEng || null,
      data.templateType,
      data.description || null,
      data.layoutConfig ? JSON.stringify(data.layoutConfig) : null,
      data.defaultQueries ? JSON.stringify(data.defaultQueries) : null,
      userId,
    ]);

    return templateId;
  }

  async deleteTemplate(templateId: string): Promise<boolean> {
    const deleteQuery = `
      DELETE FROM report_template
      WHERE template_id = $1 AND is_system = 'N'
    `;

    await query(deleteQuery, [templateId]);
    return true;
  }

  async saveAsTemplate(
    reportId: string,
    templateNameKor: string,
    templateNameEng: string | null | undefined,
    description: string | null | undefined,
    userId: string
  ): Promise<string> {
    return transaction(async (client) => {
      const reportQuery = `
        SELECT report_type FROM report_master WHERE report_id = $1
      `;
      const reportResult = await client.query(reportQuery, [reportId]);

      if (reportResult.rows.length === 0) {
        throw new Error("리포트를 찾을 수 없습니다.");
      }

      const reportType = reportResult.rows[0].report_type;

      const layoutQuery = `
        SELECT 
          canvas_width,
          canvas_height,
          page_orientation,
          margin_top,
          margin_bottom,
          margin_left,
          margin_right,
          components
        FROM report_layout
        WHERE report_id = $1
      `;
      const layoutResult = await client.query(layoutQuery, [reportId]);

      if (layoutResult.rows.length === 0) {
        throw new Error("레이아웃을 찾을 수 없습니다.");
      }

      const layout = layoutResult.rows[0];

      const queriesQuery = `
        SELECT 
          query_name,
          query_type,
          sql_query,
          parameters,
          external_connection_id,
          display_order
        FROM report_query
        WHERE report_id = $1
        ORDER BY display_order
      `;
      const queriesResult = await client.query(queriesQuery, [reportId]);

      const layoutConfig = {
        width: layout.canvas_width,
        height: layout.canvas_height,
        orientation: layout.page_orientation,
        margins: {
          top: layout.margin_top,
          bottom: layout.margin_bottom,
          left: layout.margin_left,
          right: layout.margin_right,
        },
        components: typeof layout.components === "string" ? JSON.parse(layout.components || "[]") : (layout.components || []),
      };

      const defaultQueries = queriesResult.rows.map((q) => ({
        name: q.query_name,
        type: q.query_type,
        sqlQuery: q.sql_query,
        parameters: Array.isArray(q.parameters) ? q.parameters : [],
        externalConnectionId: q.external_connection_id,
        displayOrder: q.display_order,
      }));

      const templateId = generateTemplateId();

      await client.query(
        `INSERT INTO report_template (
          template_id, template_name_kor, template_name_eng, template_type,
          is_system, description, layout_config, default_queries, use_yn, sort_order, created_by
        ) VALUES ($1, $2, $3, $4, 'N', $5, $6, $7, 'Y', 999, $8)`,
        [
          templateId, templateNameKor, templateNameEng || null, reportType,
          description || null, JSON.stringify(layoutConfig),
          JSON.stringify(defaultQueries), userId,
        ]
      );

      return templateId;
    });
  }

  async createTemplateFromLayout(
    templateNameKor: string,
    templateNameEng: string | null | undefined,
    templateType: string,
    description: string | null | undefined,
    layoutConfig: {
      width: number;
      height: number;
      orientation: string;
      margins: { top: number; bottom: number; left: number; right: number };
      components: Record<string, unknown>[];
    },
    defaultQueries: Array<{
      name: string;
      type: "MASTER" | "DETAIL";
      sqlQuery: string;
      parameters: string[];
      externalConnectionId?: number | null;
      displayOrder?: number;
    }>,
    userId: string
  ): Promise<string> {
    const templateId = generateTemplateId();

    await query(
      `INSERT INTO report_template (
        template_id, template_name_kor, template_name_eng, template_type,
        is_system, description, layout_config, default_queries, use_yn, sort_order, created_by
      ) VALUES ($1, $2, $3, $4, 'N', $5, $6, $7, 'Y', 999, $8)
      RETURNING template_id`,
      [
        templateId, templateNameKor, templateNameEng || null, templateType,
        description || null, JSON.stringify(layoutConfig),
        JSON.stringify(defaultQueries), userId,
      ]
    );

    return templateId;
  }

  // ─── 비주얼 쿼리 빌더 ─────────────────────────────────────────────────────────

  /** information_schema에서 사용자 테이블 목록 조회 */
  async getSchemaTables(): Promise<Array<{ table_name: string; table_type: string }>> {
    const sql = `
      SELECT table_name, table_type
      FROM information_schema.tables
      WHERE table_schema = 'public'
        AND table_type IN ('BASE TABLE', 'VIEW')
      ORDER BY table_name
    `;
    return query<{ table_name: string; table_type: string }>(sql, []);
  }

  /** 특정 테이블의 컬럼 정보 조회 */
  async getSchemaTableColumns(
    tableName: string
  ): Promise<Array<{ column_name: string; data_type: string; is_nullable: string }>> {
    this.validateTableName(tableName);
    const sql = `
      SELECT column_name, data_type, is_nullable
      FROM information_schema.columns
      WHERE table_schema = 'public'
        AND table_name = $1
      ORDER BY ordinal_position
    `;
    return query<{ column_name: string; data_type: string; is_nullable: string }>(sql, [tableName]);
  }

  /** VisualQuery → SELECT SQL 문자열 빌드 (순수 함수) */
  buildVisualQuerySql(vq: VisualQuery): string {
    this.validateTableName(vq.tableName);

    const selectParts: string[] = [];

    for (const col of vq.columns) {
      this.validateIdentifier(col);
      selectParts.push(`"${col}"`);
    }

    for (const fc of vq.formulaColumns) {
      this.validateFormulaExpression(fc.expression);
      this.validateIdentifier(fc.alias);
      selectParts.push(`(${fc.expression}) AS "${fc.alias}"`);
    }

    if (selectParts.length === 0) {
      throw new Error("최소 1개 이상의 컬럼을 선택해야 합니다.");
    }

    const limit = Math.min(Math.max(vq.limit ?? 100, 1), 10000);
    return `SELECT ${selectParts.join(", ")} FROM "${vq.tableName}" LIMIT ${limit}`;
  }

  async executeVisualQuery(vq: VisualQuery): Promise<{ fields: string[]; rows: Record<string, unknown>[] }> {
    const sql = this.buildVisualQuerySql(vq);
    this.validateQuerySafety(sql);

    const result = await query(sql, []);
    const fields = result.length > 0 ? Object.keys(result[0]) : [];
    return { fields, rows: result as Record<string, unknown>[] };
  }

  // ─── 비주얼 쿼리 검증 헬퍼 ─────────────────────────────────────────────────────

  /** 테이블/컬럼명 화이트리스트 검증 — 영문+숫자+밑줄만 허용 */
  private validateTableName(name: string): void {
    if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(name)) {
      throw new Error(`유효하지 않은 테이블명입니다: ${name}`);
    }
  }

  private validateIdentifier(name: string): void {
    if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(name)) {
      throw new Error(`유효하지 않은 식별자입니다: ${name}`);
    }
  }

  /** 수식 표현식 안전성 검증 — 세미콜론, 주석, 서브쿼리 금지 */
  private validateFormulaExpression(expr: string): void {
    const forbidden = [";", "--", "/*", "*/", "SELECT", "INSERT", "UPDATE", "DELETE", "DROP", "TRUNCATE"];
    const upper = expr.toUpperCase();
    for (const keyword of forbidden) {
      if (upper.includes(keyword)) {
        throw new Error(`수식에 사용할 수 없는 키워드가 포함되어 있습니다: ${keyword}`);
      }
    }
  }

  // ─── 카테고리(report_type) 관리 ─────────────────────────────────────────────────

  /** DB에 저장된 모든 카테고리(report_type) 목록 조회 (중복 제거, 정렬) */
  async getCategories(): Promise<string[]> {
    const sql = `
      SELECT DISTINCT report_type
      FROM report_master
      WHERE report_type IS NOT NULL
        AND report_type != ''
      ORDER BY report_type ASC
    `;
    const rows = await query<{ report_type: string }>(sql, []);
    return rows.map((r) => r.report_type);
  }
}

export default new ReportService();
