import { createHash, randomUUID } from 'node:crypto';
import type { Database, Queryable } from '../db/types.js';
import { ConflictError, ForbiddenError, NotFoundError } from '../domain/errors.js';
import { normalizeIdentifier } from '../domain/normalization.js';
import { appendAudit, type AuditContext } from './audit.js';
import { getAuthorization, PERMISSIONS } from './rbac.js';

export interface ManualInvoiceItem {
  productId?: string | null;
  productCode: string;
  productName: string;
  unit: string;
  quantity?: number;
}

export interface CreateManualInvoiceInput {
  invoiceNumber: string;
  requesterId: string;
  customerName?: string;
  customerCode?: string;
  notes?: string;
  items: ManualInvoiceItem[];
}

function fingerprint(identity: Record<string, unknown>, items: ManualInvoiceItem[]): string {
  const canonical = JSON.stringify({ identity, items: [...items].sort((a, b) => a.productCode.localeCompare(b.productCode)) });
  return createHash('sha256').update(canonical).digest('hex');
}

async function requirePermission(tx: Queryable, userId: string, permission: string) {
  const auth = await getAuthorization(tx, userId);
  if (!auth.permissions.has(permission)) throw new ForbiddenError();
  return auth;
}

export class InvoiceService {
  constructor(private readonly db: Database) {}

  async createManual(actorId: string, input: CreateManualInvoiceInput, audit: AuditContext): Promise<string> {
    return this.db.transaction(async (tx) => {
      const auth = await requirePermission(tx, actorId, PERMISSIONS.INVOICES_MANAGE);
      const normalized = normalizeIdentifier(input.invoiceNumber);
      if (!normalized) throw new ConflictError('شماره فاکتور الزامی است.');
      const existing = await tx.query('SELECT id FROM invoices WHERE normalized_number = $1', [normalized]);
      if (existing.rowCount) throw new ConflictError('فاکتوری با این شماره قبلاً ثبت شده است.', 'DUPLICATE_INVOICE');
      const requester = await tx.query('SELECT id FROM requesters WHERE id = $1 AND active = true AND archived_at IS NULL', [input.requesterId]);
      if (!requester.rowCount) throw new ConflictError('درخواست‌کننده معتبر نیست.');
      const ids = new Set<string>();
      for (const item of input.items) {
        const code = normalizeIdentifier(item.productCode);
        if (!code || ids.has(code)) throw new ConflictError('کد کالاها باید معتبر و یکتا باشد.');
        ids.add(code);
        if ((item.quantity ?? 1) < 1) throw new ConflictError('مقدار کالا نمی‌تواند کمتر از ۱ باشد.');
      }
      const invoiceId = randomUUID();
      const versionId = randomUUID();
      const identity = {
        invoiceNumber: input.invoiceNumber.trim(), customerCode: input.customerCode?.trim() || null,
        customerName: input.customerName?.trim() || null, notes: input.notes?.trim() || null
      };
      const normalizedItems = input.items.map((item) => ({ ...item, quantity: item.quantity ?? 1 }));
      await tx.query(
        `INSERT INTO invoices
         (id, invoice_number, normalized_number, origin, customer_code, customer_name, default_requester_id, created_by)
         VALUES ($1,$2,$3,'manual',$4,$5,$6,$7)`,
        [invoiceId, input.invoiceNumber.trim(), normalized, identity.customerCode, identity.customerName, input.requesterId, actorId]
      );
      await tx.query(
        `INSERT INTO invoice_versions
         (id, invoice_id, version_number, source, identity_data, fingerprint_sha256, created_by)
         VALUES ($1,$2,1,'manual',$3::jsonb,$4,$5)`,
        [versionId, invoiceId, JSON.stringify(identity), fingerprint(identity, normalizedItems), actorId]
      );
      for (const [index, item] of normalizedItems.entries()) {
        await tx.query(
          `INSERT INTO invoice_version_items
           (id, invoice_version_id, line_number, product_id, product_code, product_name, unit, quantity)
           VALUES ($1,$2,$3,$4,$5,$6,$7,$8)`,
          [randomUUID(), versionId, index + 1, item.productId ?? null, item.productCode, item.productName, item.unit, item.quantity]
        );
      }
      await tx.query(
        `UPDATE invoices SET manual_current_version_id = $1, operational_version_id = $1 WHERE id = $2`,
        [versionId, invoiceId]
      );
      await appendAudit(tx, { ...audit, actorId, actorRole: auth.roles.join(',') }, {
        action: 'invoice.manual_created', entityType: 'invoice', entityId: invoiceId, newValues: { ...identity, items: normalizedItems }
      });
      return invoiceId;
    });
  }

  async archive(actorId: string, invoiceId: string, reason: string, audit: AuditContext): Promise<void> {
    await this.db.transaction(async (tx) => {
      const auth = await requirePermission(tx, actorId, PERMISSIONS.INVOICES_ARCHIVE);
      if (reason.trim().length < 3) throw new ConflictError('دلیل بایگانی الزامی است.');
      const invoice = await tx.query<{ archived_at: Date | null }>('SELECT archived_at FROM invoices WHERE id = $1 FOR UPDATE', [invoiceId]);
      if (!invoice.rowCount) throw new NotFoundError('فاکتور یافت نشد.');
      if (invoice.rows[0]!.archived_at) return;
      const pending = await tx.query(
        `SELECT 1 FROM delivery_requests WHERE invoice_id = $1 AND status IN ('pending','returned') LIMIT 1`,
        [invoiceId]
      );
      if (pending.rowCount) throw new ConflictError('تا تعیین تکلیف درخواست فعال انبار، بایگانی فاکتور ممکن نیست.', 'ACTIVE_DELIVERY');
      await tx.query('UPDATE invoices SET archived_at = now(), archive_reason = $2, updated_at = now() WHERE id = $1', [invoiceId, reason.trim()]);
      await appendAudit(tx, { ...audit, actorId, actorRole: auth.roles.join(',') }, {
        action: 'invoice.archived', entityType: 'invoice', entityId: invoiceId, oldValues: { archived: false }, newValues: { archived: true }, reason
      });
    });
  }

  async restore(actorId: string, invoiceId: string, audit: AuditContext): Promise<void> {
    await this.db.transaction(async (tx) => {
      const auth = await requirePermission(tx, actorId, PERMISSIONS.INVOICES_ARCHIVE);
      const invoice = await tx.query<{ archived_at: Date | null }>('SELECT archived_at FROM invoices WHERE id = $1 FOR UPDATE', [invoiceId]);
      if (!invoice.rowCount) throw new NotFoundError('فاکتور یافت نشد.');
      await tx.query('UPDATE invoices SET archived_at = NULL, archive_reason = NULL, updated_at = now() WHERE id = $1', [invoiceId]);
      await appendAudit(tx, { ...audit, actorId, actorRole: auth.roles.join(',') }, {
        action: 'invoice.restored', entityType: 'invoice', entityId: invoiceId, oldValues: { archived: true }, newValues: { archived: false }
      });
    });
  }

  async resolveDiscrepancy(
    actorId: string,
    discrepancyId: string,
    fieldChoices: Record<string, 'financial' | 'manager'>,
    itemChoices: Record<string, 'financial' | 'manager'>,
    reason: string,
    audit: AuditContext
  ): Promise<string> {
    if (reason.trim().length < 3) throw new ConflictError('دلیل رفع مغایرت الزامی است.');
    return this.db.transaction(async (tx) => {
      const auth = await requirePermission(tx, actorId, PERMISSIONS.DISCREPANCIES_RESOLVE);
      const discrepancy = await tx.query<{
        invoice_id: string; manual_version_id: string; financial_version_id: string; status: string;
      }>('SELECT invoice_id, manual_version_id, financial_version_id, status FROM discrepancies WHERE id = $1 FOR UPDATE', [discrepancyId]);
      if (!discrepancy.rowCount) throw new NotFoundError('مغایرت یافت نشد.');
      if (discrepancy.rows[0]!.status === 'resolved') throw new ConflictError('این مغایرت قبلاً رفع شده است.');
      const row = discrepancy.rows[0]!;
      const versions = await tx.query<{ id: string; identity_data: Record<string, unknown> }>(
        `SELECT id, identity_data FROM invoice_versions WHERE id IN ($1,$2)`,
        [row.manual_version_id, row.financial_version_id]
      );
      const manual = versions.rows.find((v) => v.id === row.manual_version_id)!;
      const financial = versions.rows.find((v) => v.id === row.financial_version_id)!;
      const keys = new Set([...Object.keys(manual.identity_data), ...Object.keys(financial.identity_data)]);
      const identity: Record<string, unknown> = {};
      let managerEdited = false;
      for (const key of keys) {
        const choice = fieldChoices[key] ?? 'financial';
        identity[key] = choice === 'manager' ? manual.identity_data[key] : financial.identity_data[key];
        if (choice === 'manager') managerEdited = true;
      }
      const items = await tx.query<{ invoice_version_id: string; product_id: string | null; product_code: string; product_name: string; unit: string; quantity: string }>(
        `SELECT invoice_version_id, product_id, product_code, product_name, unit, quantity
         FROM invoice_version_items WHERE invoice_version_id IN ($1,$2)`,
        [row.manual_version_id, row.financial_version_id]
      );
      const codes = new Set(items.rows.map((item) => item.product_code));
      const chosen = [...codes].flatMap((code) => {
        const choice = itemChoices[code] ?? 'financial';
        if (choice === 'manager') managerEdited = true;
        const versionId = choice === 'manager' ? row.manual_version_id : row.financial_version_id;
        const item = items.rows.find((candidate) => candidate.product_code === code && candidate.invoice_version_id === versionId);
        return item ? [item] : [];
      });
      const versionNumber = await tx.query<{ next: number }>('SELECT COALESCE(MAX(version_number),0)+1 AS next FROM invoice_versions WHERE invoice_id = $1', [row.invoice_id]);
      const versionId = randomUUID();
      const fp = createHash('sha256').update(JSON.stringify({ identity, chosen })).digest('hex');
      await tx.query(
        `INSERT INTO invoice_versions(id, invoice_id, version_number, source, identity_data, fingerprint_sha256, created_by)
         VALUES ($1,$2,$3,'resolved',$4::jsonb,$5,$6)`,
        [versionId, row.invoice_id, Number(versionNumber.rows[0]!.next), JSON.stringify(identity), fp, actorId]
      );
      for (const [index, item] of chosen.entries()) {
        await tx.query(
          `INSERT INTO invoice_version_items
           (id, invoice_version_id, line_number, product_id, product_code, product_name, unit, quantity)
           VALUES ($1,$2,$3,$4,$5,$6,$7,$8)`,
          [randomUUID(), versionId, index + 1, item.product_id, item.product_code, item.product_name, item.unit, Number(item.quantity)]
        );
      }
      const resolution = { fields: fieldChoices, items: itemChoices, managerEdited };
      await tx.query(
        `UPDATE discrepancies SET status = 'resolved', resolution = $2::jsonb, resolution_reason = $3,
          resolved_by = $4, resolved_at = now() WHERE id = $1`,
        [discrepancyId, JSON.stringify(resolution), reason.trim(), actorId]
      );
      await tx.query(
        `UPDATE invoices SET operational_version_id = $2, has_discrepancy = false,
          manager_edited = $3, updated_at = now() WHERE id = $1`,
        [row.invoice_id, versionId, managerEdited]
      );
      await appendAudit(tx, { ...audit, actorId, actorRole: auth.roles.join(',') }, {
        action: 'discrepancy.resolved', entityType: 'discrepancy', entityId: discrepancyId,
        newValues: resolution, reason
      });
      return versionId;
    });
  }
}
