import { BadRequestException, Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { randomUUID } from 'node:crypto';
import { Repository } from 'typeorm';
import { ImportRun } from './entities/import-run.entity';
import { ImportStep } from './entities/import-step.entity';
import { IMPORT_STEP_LABELS, IMPORT_STEP_ORDER } from './imports.types';
import type {
  CellValue,
  ColumnMapping,
  ImportRunSettings,
  ImportRunSource,
  ImportStageRequest,
  ImportStepKey,
  ParsedImportFile,
  StepPreviewSummary,
} from './imports.types';
import { parseJson } from './imports.helpers';
import { parseSheets } from './imports.workbook';
import type { ImportSheetData } from './imports.workbook';
import {
  autoAssignedSheets,
  expandStageSheets,
  resolveAssignedSheets,
  suggestMapping,
  suggestStep,
} from './imports.mapping';
import { findOwnedRun } from './imports.access';
import type { ImportPrincipal } from './imports.access';

@Injectable()
export class ImportRunService {
  constructor(
    @InjectRepository(ImportRun)
    private readonly runs: Repository<ImportRun>,
    @InjectRepository(ImportStep)
    private readonly steps: Repository<ImportStep>,
  ) {}

  async createRun(
    principal: ImportPrincipal,
    source: ImportRunSource,
    file: ParsedImportFile,
    conversationId?: number | null,
    stages?: ImportStageRequest[],
    mappingByStep?: Partial<Record<ImportStepKey, ColumnMapping>>,
    settings?: ImportRunSettings,
  ) {
    if (!file.buffer || file.buffer.length === 0) {
      throw new BadRequestException('上传文件为空');
    }
    const headerRows =
      stages && stages.length > 0
        ? [...new Set(stages.map((stage) => stage.headerRow ?? 1))]
        : [1];
    const parsedByHeaderRow = new Map<number, ImportSheetData[]>();
    for (const headerRow of headerRows) {
      parsedByHeaderRow.set(
        headerRow,
        await parseSheets(file.buffer, file.originalName, file.mimeType, headerRow),
      );
    }
    const views = new Map<string, ImportSheetData>();
    if (stages && stages.length > 0) {
      for (const stage of stages) {
        for (const sheetName of expandStageSheets(stage)) {
          const view = parsedByHeaderRow
            .get(stage.headerRow ?? 1)
            ?.find((sheet) => sheet.name === sheetName);
          if (view) views.set(sheetName, view);
        }
      }
    } else {
      for (const sheet of parsedByHeaderRow.get(1) ?? []) {
        views.set(sheet.name, sheet);
      }
    }
    const sheets = [...views.values()];

    const runId = randomUUID();
    const run = this.runs.create({
      id: runId,
      userId: principal.id,
      conversationId: conversationId ?? null,
      source,
      fileName: file.originalName.slice(0, 255),
      sheetsJson: JSON.stringify(sheets),
      settingsJson: settings ? JSON.stringify(settings) : null,
      status: 'ready',
      currentStepKey: null,
      error: null,
    });

    const stepRecords: ImportStep[] = [];
    for (const stepKey of IMPORT_STEP_ORDER) {
      const assigned =
        stages && stages.length > 0
          ? resolveAssignedSheets(
              stages,
              stepKey,
              sheets.map((s) => s.name),
            )
          : autoAssignedSheets(sheets, stepKey);
      if (assigned.length === 0) {
        stepRecords.push(
          this.steps.create({
            runId,
            stepKey,
            sheetsJson: '[]',
            mappingJson: null,
            status: 'skipped',
            summaryJson: null,
            committedAt: null,
          }),
        );
        continue;
      }
      const firstSheet = sheets.find((s) => s.name === assigned[0]);
      const mapping =
        mappingByStep?.[stepKey] ?? suggestMapping(firstSheet?.headers ?? [], stepKey);
      if (mapping) {
        const allowedHeaders = new Set<string>();
        for (const sheetName of assigned) {
          const sheet = sheets.find((s) => s.name === sheetName);
          for (const header of sheet?.headers ?? []) allowedHeaders.add(header);
        }
        for (const [field, header] of Object.entries(mapping)) {
          if (header && allowedHeaders.size > 0 && !allowedHeaders.has(header)) {
            throw new BadRequestException(
              `「${IMPORT_STEP_LABELS[stepKey]}」列映射「${header}」（字段 ${field}）不在工作表表头中`,
            );
          }
        }
      }
      stepRecords.push(
        this.steps.create({
          runId,
          stepKey,
          sheetsJson: JSON.stringify(assigned),
          mappingJson: mapping ? JSON.stringify(mapping) : null,
          status: 'pending',
          summaryJson: null,
          committedAt: null,
        }),
      );
    }
    const firstActive = stepRecords.find((s) => s.status !== 'skipped');
    run.currentStepKey = firstActive?.stepKey ?? null;
    await this.runs.save(run);
    await this.steps.save(stepRecords);
    return this.getRun(principal.id, runId);
  }

  async getRun(userId: number, runId: string) {
    const run = await findOwnedRun(this.runs, userId, runId);
    const stepRecords = await this.steps.find({ where: { runId }, order: { id: 'ASC' } });
    const sheets =
      parseJson<
        Array<{
          name: string;
          headers: string[];
          rows: CellValue[][];
          headerRow?: number;
          rowNumbers?: number[];
        }>
      >(run.sheetsJson) ?? [];
    return {
      id: run.id,
      fileName: run.fileName,
      source: run.source,
      status: run.status,
      currentStepKey: run.currentStepKey,
      createdAt: run.createdAt.toISOString(),
      sheets: sheets.map((sheet) => ({
        name: sheet.name,
        headers: sheet.headers,
        rowCount: sheet.rows.length,
        suggestedStepKey: suggestStep(sheet.headers)?.stepKey ?? null,
      })),
      settings: parseJson<ImportRunSettings>(run.settingsJson) ?? {},
      steps: stepRecords.map((step) => ({
        id: step.id,
        stepKey: step.stepKey,
        label: IMPORT_STEP_LABELS[step.stepKey],
        sheets: parseJson<string[]>(step.sheetsJson) ?? [],
        status: step.status,
        mapping: parseJson<ColumnMapping>(step.mappingJson) ?? {},
        summary: parseJson<StepPreviewSummary>(step.summaryJson),
        committedAt: step.committedAt?.toISOString() ?? null,
      })),
    };
  }
}
