import { BadRequestException, ConflictException, Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { DataSource, Repository } from 'typeorm';
import { ImportRun } from './entities/import-run.entity';
import { ImportStep } from './entities/import-step.entity';
import { ImportRow } from './entities/import-row.entity';
import { IMPORT_STEP_LABELS } from './imports.types';
import type {
  CellValue,
  ColumnMapping,
  ImportRunSettings,
  ImportStepKey,
  StepPreviewSummary,
} from './imports.types';
import { parseJson } from './imports.helpers';
import { assertMapping, resolveSheetMapping, suggestMapping } from './imports.mapping';
import { buildLookups } from './imports.lookups';
import { validateRow } from './imports.rows';
import type { ImportBatchState } from './imports.rows';
import { applyPreviewPolicies } from './imports.policies';
import { findOwnedRun, findStep } from './imports.access';
import type { ImportPrincipal } from './imports.access';

@Injectable()
export class ImportPreviewService {
  constructor(
    @InjectRepository(ImportRun)
    private readonly runs: Repository<ImportRun>,
    @InjectRepository(ImportStep)
    private readonly steps: Repository<ImportStep>,
    @InjectRepository(ImportRow)
    private readonly rows: Repository<ImportRow>,
    private readonly dataSource: DataSource,
  ) {}

  async previewStep(
    principal: ImportPrincipal,
    runId: string,
    stepKey: ImportStepKey,
    body: { sheets?: string[]; mapping?: ColumnMapping },
  ) {
    const run = await findOwnedRun(this.runs, principal.id, runId);
    if (run.status === 'committed') {
      throw new BadRequestException('该导入任务已完成，无需再次预览');
    }
    if (run.status === 'committing') {
      throw new ConflictException('导入正在提交中，请稍候');
    }
    const step = await findStep(this.steps, runId, stepKey);
    if (!step || step.status === 'skipped') {
      throw new BadRequestException(`阶段「${IMPORT_STEP_LABELS[stepKey]}」没有分配工作表`);
    }
    if (step.status === 'committed') {
      throw new BadRequestException(`阶段「${IMPORT_STEP_LABELS[stepKey]}」已提交，不能重复预览`);
    }

    const sheetsData =
      parseJson<
        Array<{
          name: string;
          headers: string[];
          rows: CellValue[][];
          headerRow?: number;
          rowNumbers?: number[];
        }>
      >(run.sheetsJson) ?? [];
    const settings = parseJson<ImportRunSettings>(run.settingsJson) ?? {};
    const sheetNames = body.sheets?.length
      ? body.sheets
      : (parseJson<string[]>(step.sheetsJson) ?? []);
    const usedSheets = sheetNames.filter((name) => sheetsData.some((s) => s.name === name));
    if (usedSheets.length === 0) {
      throw new BadRequestException('指定的工作表不存在');
    }

    const mapping =
      body.mapping && Object.keys(body.mapping).length > 0
        ? body.mapping
        : (parseJson<ColumnMapping>(step.mappingJson) ??
          suggestMapping(sheetsData[0]?.headers ?? [], stepKey));
    assertMapping(stepKey, mapping, sheetsData, usedSheets);

    await this.rows.delete({ stepId: step.id });
    const rowEntities: ImportRow[] = [];
    const summary: StepPreviewSummary = {
      total: 0,
      valid: 0,
      error: 0,
      create: 0,
      update: 0,
      skip: 0,
    };
    const batchState: ImportBatchState = {
      checkinStudentIds: new Set<number>(),
      transferStudentIds: new Set<number>(),
    };

    for (const sheetName of usedSheets) {
      const sheet = sheetsData.find((s) => s.name === sheetName);
      if (!sheet) continue;
      const sheetMapping = resolveSheetMapping(mapping, sheet.headers, stepKey);
      const lookups = await buildLookups(
        this.dataSource,
        stepKey,
        sheet.headers,
        sheet.rows,
        sheetMapping,
      );
      for (let i = 0; i < sheet.rows.length; i += 1) {
        const rawValues = sheet.rows[i];
        const raw: Record<string, CellValue> = {};
        sheet.headers.forEach((header, index) => {
          raw[header] = rawValues[index] ?? null;
        });
        const fields: Record<string, CellValue> = {};
        for (const [field, header] of Object.entries(sheetMapping)) {
          fields[field] = rawValues[sheet.headers.indexOf(header)] ?? null;
        }
        const result = validateRow(stepKey, fields, lookups, batchState);
        const policy = applyPreviewPolicies(result, settings);
        const normalized = { ...result.normalized, ...result.resolvedIds };
        summary.total += 1;
        if (policy.status === 'error') {
          summary.error += 1;
        } else {
          summary.valid += 1;
          if (policy.action === 'create') summary.create += 1;
          if (policy.action === 'update') summary.update += 1;
          if (policy.action === 'skip') summary.skip += 1;
        }
        if (policy.status === 'valid' && policy.action === 'create') {
          const studentId = result.resolvedIds._studentId;
          if (studentId !== undefined) {
            if (stepKey === 'checkins') batchState.checkinStudentIds.add(studentId);
            if (stepKey === 'transfers') batchState.transferStudentIds.add(studentId);
          }
        }
        rowEntities.push(
          this.rows.create({
            runId,
            stepId: step.id,
            sheetName,
            rowNumber: sheet.rowNumbers?.[i] ?? (sheet.headerRow ?? 1) + i + 1,
            rawJson: JSON.stringify(raw),
            normalizedJson: JSON.stringify(normalized),
            matchKey: result.matchKey,
            action: policy.action,
            status: policy.status,
            errorsJson: policy.errors.length > 0 ? JSON.stringify(policy.errors) : null,
            targetId: result.targetId ?? null,
          }),
        );
      }
    }

    await this.rows.save(rowEntities);
    step.sheetsJson = JSON.stringify(usedSheets);
    step.mappingJson = JSON.stringify(mapping);
    step.status = 'ready';
    step.summaryJson = JSON.stringify(summary);
    await this.steps.save(step);

    const headers = sheetsData.find((s) => s.name === usedSheets[0])?.headers ?? [];
    const rows = rowEntities.map((entity) => ({
      id: entity.id,
      rowNumber: entity.rowNumber,
      sheetName: entity.sheetName,
      raw: parseJson<Record<string, CellValue>>(entity.rawJson) ?? {},
      fields: parseJson<Record<string, CellValue>>(entity.normalizedJson) ?? {},
      action: entity.action,
      status: entity.status,
      errors: parseJson<string[]>(entity.errorsJson) ?? [],
    }));
    return { stepKey, sheetNames: usedSheets, headers, mapping, rows, summary };
  }
}
