import {
  EstimateExportFormat,
  type EstimateExportArtifactPayload,
  type EstimateExportSummary,
  type EstimateStatus,
  type EstimateVersionStatus,
} from "@capakraken/shared";
import { summarizeEstimateDemandLines } from "./metrics.js";

type ExcelJsModule = typeof import("exceljs");

type ExportProjectRef = {
  id: string;
  name: string;
  shortCode?: string | null;
  status?: string | null;
  startDate?: Date | string | null;
  endDate?: Date | string | null;
} | null;

type ExportAssumption = {
  id: string;
  category: string;
  key: string;
  label: string;
  valueType: string;
  value: unknown;
  sortOrder: number;
  notes?: string | null;
  createdAt: Date;
  updatedAt: Date;
};

type ExportScopeItem = {
  id: string;
  sequenceNo: number;
  scopeType: string;
  packageCode?: string | null;
  name: string;
  description?: string | null;
  scene?: string | null;
  page?: string | null;
  location?: string | null;
  assumptionCategory?: string | null;
  technicalSpec: unknown;
  frameCount?: number | null;
  itemCount?: number | null;
  unitMode?: string | null;
  internalComments?: string | null;
  externalComments?: string | null;
  metadata: unknown;
  createdAt: Date;
  updatedAt: Date;
};

type ExportDemandLine = {
  id: string;
  scopeItemId?: string | null;
  roleId?: string | null;
  resourceId?: string | null;
  lineType: string;
  name: string;
  chapter?: string | null;
  hours: number;
  days?: number | null;
  fte?: number | null;
  rateSource?: string | null;
  costRateCents: number;
  billRateCents: number;
  currency: string;
  costTotalCents: number;
  priceTotalCents: number;
  monthlySpread: unknown;
  staffingAttributes: unknown;
  metadata: unknown;
  createdAt: Date;
  updatedAt: Date;
};

type ExportResourceSnapshot = {
  id: string;
  resourceId?: string | null;
  sourceEid?: string | null;
  displayName: string;
  chapter?: string | null;
  roleId?: string | null;
  currency: string;
  lcrCents: number;
  ucrCents: number;
  fte?: number | null;
  location?: string | null;
  country?: string | null;
  level?: string | null;
  workType?: string | null;
  attributes: unknown;
  createdAt: Date;
  updatedAt: Date;
};

type ExportMetric = {
  id: string;
  key: string;
  label: string;
  metricGroup?: string | null;
  valueDecimal: number;
  valueCents?: number | null;
  currency?: string | null;
  metadata: unknown;
  createdAt: Date;
  updatedAt: Date;
};

type ExportSheetRow = Record<string, unknown>;

let _excelJs: ExcelJsModule | null = null;

async function getExcelJS() {
  if (!_excelJs) {
    _excelJs = await import("exceljs");
  }

  return _excelJs;
}

export interface EstimateExportSource {
  estimate: {
    id: string;
    projectId?: string | null;
    name: string;
    opportunityId?: string | null;
    baseCurrency: string;
    status: EstimateStatus | string;
    createdAt: Date;
    updatedAt: Date;
  };
  version: {
    id: string;
    versionNumber: number;
    label?: string | null;
    status: EstimateVersionStatus | string;
    notes?: string | null;
    lockedAt?: Date | null;
    projectSnapshot: unknown;
    createdAt: Date;
    updatedAt: Date;
    assumptions: ExportAssumption[];
    scopeItems: ExportScopeItem[];
    demandLines: ExportDemandLine[];
    resourceSnapshots: ExportResourceSnapshot[];
    metrics: ExportMetric[];
  };
  project: ExportProjectRef;
}

function serializeDate(value: Date | string | null | undefined) {
  if (value instanceof Date) {
    return value.toISOString();
  }

  return value ?? null;
}

function stringifyValue(value: unknown) {
  if (value == null) {
    return "";
  }
  if (typeof value === "string") {
    return value;
  }
  if (typeof value === "number" || typeof value === "boolean") {
    return String(value);
  }

  return JSON.stringify(value);
}

function toNumericRecord(value: unknown) {
  if (typeof value !== "object" || value === null || Array.isArray(value)) {
    return {} as Record<string, number>;
  }

  return Object.fromEntries(
    Object.entries(value).filter(
      (entry): entry is [string, number] => typeof entry[1] === "number",
    ),
  );
}

function escapeDelimitedValue(value: unknown, delimiter: string) {
  const rendered = stringifyValue(value);
  if (
    rendered.includes(delimiter) ||
    rendered.includes('"') ||
    rendered.includes("\n") ||
    rendered.includes("\r")
  ) {
    return `"${rendered.replaceAll('"', '""')}"`;
  }

  return rendered;
}

function serializeDelimited(
  rows: Array<Record<string, unknown>>,
  delimiter: string,
) {
  if (rows.length === 0) {
    return "";
  }

  const columns = Array.from(
    rows.reduce((keys, row) => {
      for (const key of Object.keys(row)) {
        keys.add(key);
      }
      return keys;
    }, new Set<string>()),
  );
  const header = columns.map((column) => escapeDelimitedValue(column, delimiter));
  const body = rows.map((row) =>
    columns.map((column) => escapeDelimitedValue(row[column], delimiter)).join(delimiter),
  );

  return [header.join(delimiter), ...body].join("\n");
}

function buildSummary(source: EstimateExportSource): EstimateExportSummary {
  const summarized = summarizeEstimateDemandLines(source.version.demandLines);
  const metricsByKey = new Map(
    source.version.metrics.map((metric) => [metric.key, metric]),
  );

  const totalHours =
    metricsByKey.get("total_hours")?.valueDecimal ?? summarized.totalHours;
  const totalCostCents =
    metricsByKey.get("total_cost")?.valueCents ?? summarized.totalCostCents;
  const totalPriceCents =
    metricsByKey.get("total_price")?.valueCents ?? summarized.totalPriceCents;
  const marginCents =
    metricsByKey.get("margin")?.valueCents ?? summarized.marginCents;
  const marginPercent =
    metricsByKey.get("margin_percent")?.valueDecimal ?? summarized.marginPercent;

  return {
    estimateId: source.estimate.id,
    estimateName: source.estimate.name,
    versionId: source.version.id,
    versionNumber: source.version.versionNumber,
    versionStatus: source.version.status as EstimateVersionStatus,
    projectId: source.estimate.projectId ?? source.project?.id ?? null,
    projectName: source.project?.name ?? null,
    baseCurrency: source.estimate.baseCurrency,
    assumptionCount: source.version.assumptions.length,
    scopeItemCount: source.version.scopeItems.length,
    demandLineCount: source.version.demandLines.length,
    resourceSnapshotCount: source.version.resourceSnapshots.length,
    totalHours,
    totalCostCents,
    totalPriceCents,
    marginCents,
    marginPercent,
  };
}

function buildOverviewRows(
  source: EstimateExportSource,
  summary: EstimateExportSummary,
) {
  return [
    { field: "estimate_id", value: summary.estimateId },
    { field: "estimate_name", value: summary.estimateName },
    { field: "estimate_status", value: source.estimate.status },
    { field: "version_id", value: summary.versionId },
    { field: "version_number", value: summary.versionNumber },
    { field: "version_status", value: summary.versionStatus },
    { field: "version_label", value: source.version.label ?? "" },
    { field: "version_notes", value: source.version.notes ?? "" },
    { field: "project_id", value: summary.projectId ?? "" },
    { field: "project_name", value: summary.projectName ?? "" },
    { field: "project_code", value: source.project?.shortCode ?? "" },
    { field: "base_currency", value: summary.baseCurrency },
    { field: "opportunity_id", value: source.estimate.opportunityId ?? "" },
    { field: "locked_at", value: serializeDate(source.version.lockedAt) ?? "" },
    { field: "generated_from_project_start", value: serializeDate(source.project?.startDate) ?? "" },
    { field: "generated_from_project_end", value: serializeDate(source.project?.endDate) ?? "" },
    { field: "assumption_count", value: summary.assumptionCount },
    { field: "scope_item_count", value: summary.scopeItemCount },
    { field: "demand_line_count", value: summary.demandLineCount },
    { field: "resource_snapshot_count", value: summary.resourceSnapshotCount },
    { field: "total_hours", value: summary.totalHours },
    { field: "total_cost_cents", value: summary.totalCostCents },
    { field: "total_price_cents", value: summary.totalPriceCents },
    { field: "margin_cents", value: summary.marginCents },
    { field: "margin_percent", value: summary.marginPercent },
  ];
}

function buildAssumptionRows(assumptions: ExportAssumption[]) {
  return assumptions.map((assumption) => ({
    id: assumption.id,
    category: assumption.category,
    key: assumption.key,
    label: assumption.label,
    value_type: assumption.valueType,
    value: stringifyValue(assumption.value),
    notes: assumption.notes ?? "",
    sort_order: assumption.sortOrder,
  }));
}

function buildScopeRows(scopeItems: ExportScopeItem[]) {
  return scopeItems.map((scopeItem) => ({
    id: scopeItem.id,
    sequence_no: scopeItem.sequenceNo,
    scope_type: scopeItem.scopeType,
    package_code: scopeItem.packageCode ?? "",
    name: scopeItem.name,
    description: scopeItem.description ?? "",
    scene: scopeItem.scene ?? "",
    page: scopeItem.page ?? "",
    location: scopeItem.location ?? "",
    assumption_category: scopeItem.assumptionCategory ?? "",
    frame_count: scopeItem.frameCount ?? "",
    item_count: scopeItem.itemCount ?? "",
    unit_mode: scopeItem.unitMode ?? "",
    technical_spec: stringifyValue(scopeItem.technicalSpec),
    internal_comments: scopeItem.internalComments ?? "",
    external_comments: scopeItem.externalComments ?? "",
    metadata: stringifyValue(scopeItem.metadata),
  }));
}

function buildDemandRows(source: EstimateExportSource) {
  const scopeItemsById = new Map(
    source.version.scopeItems.map((scopeItem) => [scopeItem.id, scopeItem]),
  );

  return source.version.demandLines.map((line, index) => ({
    line_no: index + 1,
    line_id: line.id,
    scope_item_id: line.scopeItemId ?? "",
    scope_item_name:
      (line.scopeItemId ? scopeItemsById.get(line.scopeItemId)?.name : null) ?? "",
    role_id: line.roleId ?? "",
    resource_id: line.resourceId ?? "",
    line_type: line.lineType,
    name: line.name,
    chapter: line.chapter ?? "",
    hours: line.hours,
    days: line.days ?? "",
    fte: line.fte ?? "",
    rate_source: line.rateSource ?? "",
    cost_rate_cents: line.costRateCents,
    bill_rate_cents: line.billRateCents,
    currency: line.currency,
    cost_total_cents: line.costTotalCents,
    price_total_cents: line.priceTotalCents,
    monthly_spread: stringifyValue(toNumericRecord(line.monthlySpread)),
    staffing_attributes: stringifyValue(line.staffingAttributes),
    metadata: stringifyValue(line.metadata),
  }));
}

function buildResourceRows(resourceSnapshots: ExportResourceSnapshot[]) {
  return resourceSnapshots.map((snapshot) => ({
    id: snapshot.id,
    resource_id: snapshot.resourceId ?? "",
    source_eid: snapshot.sourceEid ?? "",
    display_name: snapshot.displayName,
    chapter: snapshot.chapter ?? "",
    role_id: snapshot.roleId ?? "",
    currency: snapshot.currency,
    lcr_cents: snapshot.lcrCents,
    ucr_cents: snapshot.ucrCents,
    fte: snapshot.fte ?? "",
    location: snapshot.location ?? "",
    country: snapshot.country ?? "",
    level: snapshot.level ?? "",
    work_type: snapshot.workType ?? "",
    attributes: stringifyValue(snapshot.attributes),
  }));
}

function buildMetricRows(metrics: ExportMetric[]) {
  return metrics.map((metric) => ({
    id: metric.id,
    key: metric.key,
    label: metric.label,
    metric_group: metric.metricGroup ?? "",
    value_decimal: metric.valueDecimal,
    value_cents: metric.valueCents ?? "",
    currency: metric.currency ?? "",
    metadata: stringifyValue(metric.metadata),
  }));
}

function buildSapRows(
  source: EstimateExportSource,
  summary: EstimateExportSummary,
) {
  return source.version.demandLines.map((line, index) => ({
    record_type: "ESTIMATE_LINE",
    estimate_id: summary.estimateId,
    version_number: summary.versionNumber,
    project_code: source.project?.shortCode ?? "",
    project_name: summary.projectName ?? "",
    line_no: index + 1,
    line_name: line.name,
    role_id: line.roleId ?? "",
    resource_id: line.resourceId ?? "",
    chapter: line.chapter ?? "",
    hours: line.hours,
    cost_rate_cents: line.costRateCents,
    bill_rate_cents: line.billRateCents,
    cost_total_cents: line.costTotalCents,
    price_total_cents: line.priceTotalCents,
    currency: line.currency,
    rate_source: line.rateSource ?? "",
    version_status: summary.versionStatus,
  }));
}

function buildMmpRows(
  source: EstimateExportSource,
  summary: EstimateExportSummary,
) {
  const monthKeys = Array.from(
    new Set(
      source.version.demandLines.flatMap((line) =>
        Object.keys(toNumericRecord(line.monthlySpread)),
      ),
    ),
  ).sort();

  return source.version.demandLines.map((line, index) => {
    const baseRow: Record<string, unknown> = {
      estimate_id: summary.estimateId,
      version_number: summary.versionNumber,
      project_id: summary.projectId ?? "",
      project_code: source.project?.shortCode ?? "",
      line_no: index + 1,
      line_name: line.name,
      role_id: line.roleId ?? "",
      resource_id: line.resourceId ?? "",
      total_hours: line.hours,
      total_cost_cents: line.costTotalCents,
      total_price_cents: line.priceTotalCents,
      currency: line.currency,
    };

    for (const monthKey of monthKeys) {
      baseRow[`month_${monthKey}`] =
        toNumericRecord(line.monthlySpread)[monthKey] ?? 0;
    }

    return baseRow;
  });
}

function buildJsonDocument(
  source: EstimateExportSource,
  summary: EstimateExportSummary,
) {
  return {
    schemaVersion: 1,
    generatedAt: new Date().toISOString(),
    estimate: {
      ...source.estimate,
      createdAt: source.estimate.createdAt.toISOString(),
      updatedAt: source.estimate.updatedAt.toISOString(),
    },
    project: source.project
      ? {
          ...source.project,
          startDate: serializeDate(source.project.startDate),
          endDate: serializeDate(source.project.endDate),
        }
      : null,
    version: {
      id: source.version.id,
      versionNumber: source.version.versionNumber,
      label: source.version.label ?? null,
      status: source.version.status,
      notes: source.version.notes ?? null,
      lockedAt: serializeDate(source.version.lockedAt),
      projectSnapshot: source.version.projectSnapshot,
      createdAt: source.version.createdAt.toISOString(),
      updatedAt: source.version.updatedAt.toISOString(),
    },
    summary,
    assumptions: source.version.assumptions.map((assumption) => ({
      ...assumption,
      createdAt: assumption.createdAt.toISOString(),
      updatedAt: assumption.updatedAt.toISOString(),
    })),
    scopeItems: source.version.scopeItems.map((scopeItem) => ({
      ...scopeItem,
      createdAt: scopeItem.createdAt.toISOString(),
      updatedAt: scopeItem.updatedAt.toISOString(),
    })),
    demandLines: source.version.demandLines.map((line) => ({
      ...line,
      createdAt: line.createdAt.toISOString(),
      updatedAt: line.updatedAt.toISOString(),
    })),
    resourceSnapshots: source.version.resourceSnapshots.map((snapshot) => ({
      ...snapshot,
      createdAt: snapshot.createdAt.toISOString(),
      updatedAt: snapshot.updatedAt.toISOString(),
    })),
    metrics: source.version.metrics.map((metric) => ({
      ...metric,
      createdAt: metric.createdAt.toISOString(),
      updatedAt: metric.updatedAt.toISOString(),
    })),
  };
}

function base64ByteLength(content: string) {
  const padding = content.endsWith("==") ? 2 : content.endsWith("=") ? 1 : 0;
  return Math.floor((content.length * 3) / 4) - padding;
}

function buildSheetColumns(rows: ExportSheetRow[]) {
  return Array.from(
    rows.reduce((keys, row) => {
      for (const key of Object.keys(row)) {
        keys.add(key);
      }
      return keys;
    }, new Set<string>()),
  );
}

function toWorksheetCellValue(value: unknown): boolean | Date | number | string {
  if (value == null) {
    return "";
  }

  if (value instanceof Date) {
    return value;
  }

  if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
    return value;
  }

  return stringifyValue(value);
}

function appendWorksheetFromRows(
  workbook: InstanceType<ExcelJsModule["Workbook"]>,
  sheetName: string,
  rows: ExportSheetRow[],
): void {
  const worksheet = workbook.addWorksheet(sheetName);
  const columns = buildSheetColumns(rows);

  if (columns.length === 0) {
    return;
  }

  worksheet.addRow(columns);

  for (const row of rows) {
    worksheet.addRow(columns.map((column) => toWorksheetCellValue(row[column])));
  }
}

function buildTextPayload(
  format: EstimateExportFormat,
  content: string,
  summary: EstimateExportSummary,
  options: {
    mimeType: string;
    fileExtension: string;
    rowCount: number;
  },
): EstimateExportArtifactPayload {
  const lineCount = content.length === 0 ? 0 : content.split("\n").length;

  return {
    schemaVersion: 1,
    format,
    mimeType: options.mimeType,
    encoding: "utf8",
    fileExtension: options.fileExtension,
    generatedAt: new Date().toISOString(),
    byteLength: new TextEncoder().encode(content).length,
    rowCount: options.rowCount,
    lineCount,
    previewText: content.split("\n").slice(0, 12).join("\n"),
    content,
    summary,
  };
}

async function buildXlsxPayload(
  source: EstimateExportSource,
  summary: EstimateExportSummary,
): Promise<EstimateExportArtifactPayload> {
  const overviewRows = buildOverviewRows(source, summary);
  const assumptionRows = buildAssumptionRows(source.version.assumptions);
  const scopeRows = buildScopeRows(source.version.scopeItems);
  const demandRows = buildDemandRows(source);
  const resourceRows = buildResourceRows(source.version.resourceSnapshots);
  const metricRows = buildMetricRows(source.version.metrics);
  const ExcelJS = await getExcelJS();
  const workbook = new ExcelJS.Workbook();
  const sheets = [
    { name: "Overview", rows: overviewRows },
    { name: "Assumptions", rows: assumptionRows },
    { name: "Scope", rows: scopeRows },
    { name: "DemandLines", rows: demandRows },
    { name: "Resources", rows: resourceRows },
    { name: "Metrics", rows: metricRows },
  ] as const;

  for (const sheet of sheets) {
    appendWorksheetFromRows(workbook, sheet.name, sheet.rows);
  }

  const buffer = await workbook.xlsx.writeBuffer();
  const content = Buffer.from(buffer).toString("base64");

  return {
    schemaVersion: 1,
    format: EstimateExportFormat.XLSX,
    mimeType:
      "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
    encoding: "base64",
    fileExtension: "xlsx",
    generatedAt: new Date().toISOString(),
    byteLength: base64ByteLength(content),
    rowCount:
      overviewRows.length +
      assumptionRows.length +
      scopeRows.length +
      demandRows.length +
      resourceRows.length +
      metricRows.length,
    lineCount: null,
    sheetNames: sheets.map((sheet) => sheet.name),
    previewText: `Sheets: ${sheets.map((sheet) => sheet.name).join(", ")}`,
    content,
    summary,
  };
}

export async function serializeEstimateExport(
  source: EstimateExportSource,
  format: EstimateExportFormat,
): Promise<EstimateExportArtifactPayload> {
  const summary = buildSummary(source);

  if (format === EstimateExportFormat.JSON) {
    const content = JSON.stringify(buildJsonDocument(source, summary), null, 2);
    return buildTextPayload(format, content, summary, {
      mimeType: "application/json; charset=utf-8",
      fileExtension: "json",
      rowCount: summary.demandLineCount,
    });
  }

  if (format === EstimateExportFormat.CSV) {
    const rows = buildDemandRows(source);
    return buildTextPayload(format, serializeDelimited(rows, ","), summary, {
      mimeType: "text/csv; charset=utf-8",
      fileExtension: "csv",
      rowCount: rows.length,
    });
  }

  if (format === EstimateExportFormat.SAP) {
    const rows = buildSapRows(source, summary);
    return buildTextPayload(format, serializeDelimited(rows, ";"), summary, {
      mimeType: "text/plain; charset=utf-8",
      fileExtension: "sap",
      rowCount: rows.length,
    });
  }

  if (format === EstimateExportFormat.MMP) {
    const rows = buildMmpRows(source, summary);
    return buildTextPayload(format, serializeDelimited(rows, "|"), summary, {
      mimeType: "text/plain; charset=utf-8",
      fileExtension: "mmp",
      rowCount: rows.length,
    });
  }

  return buildXlsxPayload(source, summary);
}
