import { BadRequestException, Injectable, Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';

import { ClauseLibrary, LibraryScope } from '../entities/clause-library.entity';
import {
  ISO9001_FULL_CLAUSES,
  ISO9001_READINESS_ITEMS,
  ISO9001_EXTRA_ELEMENTS,
  SeedRow,
} from '../seed/iso9001.seed';
import { ISO14001_FULL_CLAUSES, ISO14001_READINESS_ITEMS, ISO14001_EXTRA_ELEMENTS } from '../seed/iso14001.seed';
import { IMS_FULL_CLAUSES, IMS_READINESS_ITEMS, IMS_EXTRA_ELEMENTS } from '../seed/ims.seed';
import { ISO45001_FULL_CLAUSES, ISO45001_READINESS_ITEMS, ISO45001_EXTRA_ELEMENTS } from '../seed/iso45001.seed';
import { ISO14001_2015_FULL_CLAUSES, ISO14001_2015_READINESS_ITEMS, ISO14001_2015_EXTRA_ELEMENTS } from '../seed/iso14001-2015.seed';

/** Built-in clause sets. Key = the last part of POST /audit-reports/clause-library/seed/:key */
export const SEED_SETS: Record<string, { label: string; readiness: SeedRow[]; full: SeedRow[] }> = {
  iso9001: { label: 'ISO 9001:2015 (Form 04-2a)', readiness: ISO9001_READINESS_ITEMS, full: [...ISO9001_FULL_CLAUSES, ...ISO9001_EXTRA_ELEMENTS] },
  iso14001: { label: 'ISO 14001:2026 (Form 04-2b)', readiness: ISO14001_READINESS_ITEMS, full: [...ISO14001_FULL_CLAUSES, ...ISO14001_EXTRA_ELEMENTS] },
  iso14001_2015: { label: 'ISO 14001:2015 (Form 04-2b)', readiness: ISO14001_2015_READINESS_ITEMS, full: [...ISO14001_2015_FULL_CLAUSES, ...ISO14001_2015_EXTRA_ELEMENTS] },
  ims: { label: 'IMS — ISO 9001 + 14001 + 45001 (Form 04-2a integrated)', readiness: IMS_READINESS_ITEMS, full: [...IMS_FULL_CLAUSES, ...IMS_EXTRA_ELEMENTS] },
  iso45001: { label: 'ISO 45001:2018 (Form 04-2a OH&S)', readiness: ISO45001_READINESS_ITEMS, full: [...ISO45001_FULL_CLAUSES, ...ISO45001_EXTRA_ELEMENTS] },
};

@Injectable()
export class ClauseLibraryService {
  private readonly logger = new Logger(ClauseLibraryService.name);

  constructor(
    @InjectRepository(ClauseLibrary, 'scheme_dbs')
    private readonly repo: Repository<ClauseLibrary>,
  ) {}

  list(standardId: number, scope?: LibraryScope) {
    return this.repo.find({
      where: { standard_id: standardId, ...(scope ? { scope } : {}) },
      order: { scope: 'ASC', sort_order: 'ASC', id: 'ASC' },
    });
  }

  /**
   * Insert the ISO 9001:2015 clause set taken from the Form 04-2a Word reports.
   * Safe to run more than once: rows that already exist (same standard/scope/group/code) are skipped,
   * and existing rows are never overwritten, so edits made in the library are kept.
   */
  seedIso9001(standardId: number, overwriteEvidence = false) {
    return this.seedStandard('iso9001', standardId, overwriteEvidence);
  }

  /** Same for any built-in clause set (iso9001, iso14001, ...). */
  async seedStandard(setKey: string, standardId: number, overwriteEvidence = false): Promise<{ set: string; inserted: number; skipped: number; evidence_filled: number }> {
    const set = SEED_SETS[String(setKey).toLowerCase()];
    if (!set) throw new BadRequestException(`Unknown clause set "${setKey}". Available: ${Object.keys(SEED_SETS).join(', ')}.`);
    const plan: { scope: LibraryScope; rows: SeedRow[] }[] = [
      { scope: 'readiness', rows: set.readiness },
      { scope: 'full', rows: set.full },
    ];

    const existing = await this.repo.find({ where: { standard_id: standardId } });
    const have = new Set(existing.map((e) => `${e.scope}|${e.group_key}|${e.code}`));
    const byKey = new Map(existing.map((e) => [`${e.scope}|${e.group_key}|${e.code}`, e]));
    let evidenceFilled = 0;

    let inserted = 0;
    let skipped = 0;
    for (const { scope, rows } of plan) {
      let order = 0;
      for (const r of rows) {
        order += 1;
        const key = `${scope}|${r.group_key}|${r.code}`;
        if (have.has(key)) {
          skipped += 1;
          // never overwrite the library, but fill the original evidence wording when it is still empty
          const row = byKey.get(key)!;
          if (r.default_evidence && (!row.default_evidence || (overwriteEvidence && row.default_evidence !== r.default_evidence))) {
            row.default_evidence = r.default_evidence;
            await this.repo.save(row);
            evidenceFilled += 1;
          }
          continue;
        }
        await this.repo.save(
          this.repo.create({
            standard_id: standardId,
            scope,
            group_key: r.group_key,
            code: r.code,
            heading: r.heading,
            title: r.title || '',
            requirement: r.requirement || null,
            default_evidence: r.default_evidence || null,
            sort_order: order,
            is_active: true,
          }),
        );
        have.add(key);
        inserted += 1;
      }
    }
    this.logger.log(`[CLAUSE-LIB] ${set.label} seed for standard ${standardId}: +${inserted}, skipped ${skipped}, evidence filled ${evidenceFilled}`);
    return { set: set.label, inserted, skipped, evidence_filled: evidenceFilled };
  }

  /**
   * Add or update clause rows for any standard (used for ISO 27001, 14001, 45001 ...).
   * Rows are matched on standard + scope + group + code: an existing row gets the new text, a new code is inserted.
   * Reports that already exist are not touched (they hold their own copy of the text).
   * Row order follows the order sent.
   */
  async importRows(
    standardId: number,
    scope: LibraryScope,
    rows: { group_key: any; code: string; heading: string; title?: string; requirement?: string; default_evidence?: string }[],
  ) {
    const existing = await this.repo.find({ where: { standard_id: standardId, scope } });
    const byKey = new Map(existing.map((e) => [`${e.group_key}|${e.code}`, e]));
    let inserted = 0;
    let updated = 0;
    const seen = new Set<string>();
    let order = 0;
    for (const r of rows) {
      const key = `${r.group_key}|${r.code.trim()}`;
      if (seen.has(key)) throw new BadRequestException(`Duplicate row in the import: ${r.group_key} ${r.code}`);
      seen.add(key);
      order += 1;
      const row = byKey.get(key) ?? this.repo.create({ standard_id: standardId, scope, group_key: r.group_key, code: r.code.trim() });
      const isNew = !row.id;
      row.heading = r.heading;
      row.title = r.title ?? '';
      row.requirement = r.requirement ?? null;
      row.default_evidence = r.default_evidence ?? null;
      row.sort_order = order;
      row.is_active = true;
      await this.repo.save(row);
      if (isNew) inserted += 1; else updated += 1;
    }
    return { inserted, updated, total: rows.length };
  }

  /** Copy every row of one standard to another (rows that already exist on the target are skipped). Starting point only: edit the text afterwards with importRows. */
  async cloneStandard(fromId: number, toId: number) {
    if (fromId === toId) throw new BadRequestException('Source and target standard are the same.');
    const src = await this.repo.find({ where: { standard_id: fromId }, order: { scope: 'ASC', sort_order: 'ASC' } });
    if (!src.length) throw new BadRequestException(`Standard ${fromId} has no clause library rows to copy.`);
    const have = new Set((await this.repo.find({ where: { standard_id: toId } })).map((e) => `${e.scope}|${e.group_key}|${e.code}`));
    let inserted = 0;
    for (const r of src) {
      if (have.has(`${r.scope}|${r.group_key}|${r.code}`)) continue;
      await this.repo.save(this.repo.create({
        standard_id: toId, scope: r.scope, group_key: r.group_key, code: r.code, heading: r.heading, title: r.title,
        requirement: r.requirement, default_evidence: r.default_evidence, sort_order: r.sort_order, is_active: true,
      }));
      inserted += 1;
    }
    return { inserted, skipped: src.length - inserted };
  }
}
