import { randomUUID } from 'node:crypto';
import type { Database, Queryable } from '../db/types.js';
import { ConflictError, ForbiddenError, NotFoundError } from '../domain/errors.js';
import { nextWarehouseState, validateApprovedQuantity, warehouseEventType, type DeliveryRequestState, type WarehouseAction } from '../domain/delivery.js';
import { appendAudit, type AuditContext } from './audit.js';
import { getAuthorization, PERMISSIONS } from './rbac.js';

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

export interface ApproveDeliveryInput {
  invoiceId: string;
  requesterId: string;
  warehouseId: string;
  deliveryType: 'complete' | 'partial';
  wholeInvoiceNoLines?: boolean;
  items: DeliveryApprovalItem[];
  notes?: string;
  idempotencyKey: string;
}

export type RealtimePublisher = (channel: string, payload: Record<string, unknown>) => Promise<void>;

async function assertPermission(tx: Queryable, userId: string, code: string): Promise<string[]> {
  const auth = await getAuthorization(tx, userId);
  if (!auth.permissions.has(code)) throw new ForbiddenError();
  return auth.roles;
}

export class DeliveryService {
  constructor(private readonly db: Database, private readonly publish: RealtimePublisher = async () => undefined) {}

  async approve(actor: { id: string }, input: ApproveDeliveryInput, audit: AuditContext): Promise<{ id: string; status: string }> {
    const result = await this.db.transaction(async (tx) => {
      const roles = await assertPermission(tx, actor.id, PERMISSIONS.DELIVERIES_APPROVE);
      const duplicate = await tx.query<{ delivery_request_id: string }>(
        `SELECT delivery_request_id FROM delivery_events WHERE actor_id = $1 AND idempotency_key = $2`,
        [actor.id, input.idempotencyKey]
      );
      if (duplicate.rowCount) return { id: duplicate.rows[0]!.delivery_request_id, status: 'pending', duplicate: true, warehouseId: input.warehouseId };
      const invoice = await tx.query<{ id: string; archived_at: Date | null; operational_version_id: string | null; invoice_number: string }>(
        `SELECT id, archived_at, operational_version_id, invoice_number FROM invoices WHERE id = $1 FOR UPDATE`,
        [input.invoiceId]
      );
      if (!invoice.rowCount) throw new NotFoundError('فاکتور یافت نشد.');
      if (invoice.rows[0]!.archived_at) throw new ConflictError('فاکتور بایگانی‌شده قابل ارسال به انبار نیست.', 'ARCHIVED_INVOICE');
      const requester = await tx.query<{ id: string }>('SELECT id FROM requesters WHERE id = $1 AND active = true AND archived_at IS NULL', [input.requesterId]);
      if (!requester.rowCount) throw new ConflictError('درخواست‌کننده فعال نیست.', 'INACTIVE_REQUESTER');
      const warehouse = await tx.query<{ id: string }>('SELECT id FROM warehouses WHERE id = $1 AND active = true AND archived_at IS NULL', [input.warehouseId]);
      if (!warehouse.rowCount) throw new ConflictError('انبار فعال نیست.', 'INACTIVE_WAREHOUSE');
      if (input.wholeInvoiceNoLines) {
        if (input.deliveryType !== 'complete' || input.items.length) throw new ConflictError('تحویل بدون ردیف فقط به‌صورت تحویل کامل مجاز است.');
      } else {
        if (!input.items.length) throw new ConflictError('حداقل یک قلم باید برای تحویل انتخاب شود.');
        const codes = input.items.map((item) => item.productCode);
        if (new Set(codes).size !== codes.length) throw new ConflictError('هر کالا فقط یک‌بار قابل انتخاب است.');
        const remaining = await this.remainingQuantities(tx, input.invoiceId);
        for (const item of input.items) validateApprovedQuantity(item.quantity, remaining.get(item.productCode) ?? 0);
        if (input.deliveryType === 'complete') {
          const allRemaining = [...remaining.values()].reduce((sum, value) => sum + value, 0);
          const selected = input.items.reduce((sum, item) => sum + item.quantity, 0);
          if (Math.abs(allRemaining - selected) > 0.0001) throw new ConflictError('برای تحویل کامل باید تمام مقدار باقیمانده انتخاب شود.', 'INCOMPLETE_FULL_DELIVERY');
        }
      }
      const id = randomUUID();
      await tx.query(
        `INSERT INTO delivery_requests
         (id, invoice_id, requester_id, warehouse_id, delivery_type, whole_invoice_no_lines, notes, approved_by)
         VALUES ($1,$2,$3,$4,$5,$6,$7,$8)`,
        [id, input.invoiceId, input.requesterId, input.warehouseId, input.deliveryType, input.wholeInvoiceNoLines ?? false, input.notes ?? null, actor.id]
      );
      for (const item of input.items) {
        await tx.query(
          `INSERT INTO delivery_request_items
           (id, delivery_request_id, product_id, product_code, product_name, unit, approved_quantity)
           VALUES ($1,$2,$3,$4,$5,$6,$7)`,
          [randomUUID(), id, item.productId ?? null, item.productCode, item.productName, item.unit, item.quantity]
        );
      }
      const eventId = randomUUID();
      await tx.query(
        `INSERT INTO delivery_events(id, delivery_request_id, sequence, event_type, actor_id, idempotency_key, metadata)
         VALUES ($1,$2,1,'approved',$3,$4,$5::jsonb)`,
        [eventId, id, actor.id, input.idempotencyKey, JSON.stringify({ deliveryType: input.deliveryType, itemCount: input.items.length })]
      );
      await tx.query(
        `INSERT INTO notifications(id, user_id, kind, title, body, data)
         SELECT md5($1::text || u.id::text)::uuid, u.id, 'delivery_approved', 'درخواست تحویل جدید',
          'فاکتور ' || $2::text || ' برای تحویل تأیید شد.', jsonb_build_object('deliveryRequestId', $3::text)
         FROM users u JOIN user_roles ur ON ur.user_id = u.id JOIN roles r ON r.id = ur.role_id
         WHERE r.code = 'warehouse_operator' AND u.warehouse_id = $4::uuid AND u.active = true AND u.archived_at IS NULL`,
        [randomUUID(), invoice.rows[0]!.invoice_number, id, input.warehouseId]
      );
      await appendAudit(tx, { ...audit, actorId: actor.id, actorRole: roles.join(',') }, {
        action: 'delivery.approved', entityType: 'delivery_request', entityId: id,
        newValues: { invoiceId: input.invoiceId, deliveryType: input.deliveryType, items: input.items }, deliveryEventId: eventId
      });
      return { id, status: 'pending', duplicate: false, warehouseId: input.warehouseId };
    });
    if (!result.duplicate) await this.publish(`warehouse:${result.warehouseId}`, { type: 'delivery.approved', requestId: result.id });
    return { id: result.id, status: result.status };
  }

  async warehouseAction(
    actor: { id: string; warehouseId: string | null },
    requestId: string,
    action: WarehouseAction,
    idempotencyKey: string,
    reason: string | undefined,
    audit: AuditContext
  ): Promise<{ id: string; state: DeliveryRequestState; duplicate: boolean }> {
    const result = await this.db.transaction(async (tx) => {
      const roles = await assertPermission(tx, actor.id, PERMISSIONS.DELIVERIES_WAREHOUSE_ACT);
      const prior = await tx.query<{ delivery_request_id: string; event_type: string }>(
        `SELECT delivery_request_id, event_type FROM delivery_events WHERE actor_id = $1 AND idempotency_key = $2`,
        [actor.id, idempotencyKey]
      );
      if (prior.rowCount) {
        const current = await tx.query<{ status: DeliveryRequestState }>('SELECT status FROM delivery_requests WHERE id = $1', [prior.rows[0]!.delivery_request_id]);
        return { id: prior.rows[0]!.delivery_request_id, state: current.rows[0]!.status, duplicate: true, approvedBy: null as string | null };
      }
      if ((action === 'cancel' || action === 'return') && (!reason || reason.trim().length < 3)) {
        throw new ConflictError('ثبت دلیل با حداقل سه نویسه الزامی است.', 'REASON_REQUIRED');
      }
      const request = await tx.query<{ status: DeliveryRequestState; warehouse_id: string; approved_by: string; invoice_number: string }>(
        `SELECT dr.status, dr.warehouse_id, dr.approved_by, i.invoice_number
         FROM delivery_requests dr JOIN invoices i ON i.id = dr.invoice_id
         WHERE dr.id = $1 FOR UPDATE`,
        [requestId]
      );
      if (!request.rowCount) throw new NotFoundError('درخواست تحویل یافت نشد.');
      if (!roles.includes('super_admin') && (!actor.warehouseId || request.rows[0]!.warehouse_id !== actor.warehouseId)) {
        throw new ForbiddenError('این درخواست به انبار شما مربوط نیست.');
      }
      const current = request.rows[0]!.status;
      const state = nextWarehouseState(current, action);
      const eventType = warehouseEventType(current, action);
      const sequence = await tx.query<{ next: number }>(
        `SELECT COALESCE(MAX(sequence), 0) + 1 AS next FROM delivery_events WHERE delivery_request_id = $1`,
        [requestId]
      );
      const eventId = randomUUID();
      await tx.query(
        `INSERT INTO delivery_events
         (id, delivery_request_id, sequence, event_type, actor_id, reason, idempotency_key)
         VALUES ($1,$2,$3,$4,$5,$6,$7)`,
        [eventId, requestId, Number(sequence.rows[0]!.next), eventType, actor.id, reason?.trim() ?? null, idempotencyKey]
      );
      const changed = await tx.query(
        `UPDATE delivery_requests SET status = $1, state_version = state_version + 1
         WHERE id = $2 AND status = $3`,
        [state, requestId, current]
      );
      if (changed.rowCount !== 1) throw new ConflictError('وضعیت درخواست هم‌زمان تغییر کرده است؛ صفحه را تازه کنید.', 'CONCURRENT_CHANGE');
      const labels: Record<string, string> = { delivered: 'تحویل شد', cancelled: 'لغو شد', returned: 'برگشت خورد', redelivered: 'دوباره تحویل شد' };
      await tx.query(
        `INSERT INTO notifications(id, user_id, kind, title, body, data)
         VALUES ($1,$2,$3,'به‌روزرسانی تحویل',$4,$5::jsonb)`,
        [randomUUID(), request.rows[0]!.approved_by, `delivery_${eventType}`, `فاکتور ${request.rows[0]!.invoice_number} ${labels[eventType]}.`, JSON.stringify({ deliveryRequestId: requestId })]
      );
      await appendAudit(tx, { ...audit, actorId: actor.id, actorRole: roles.join(',') }, {
        action: `delivery.${eventType}`, entityType: 'delivery_request', entityId: requestId,
        oldValues: { status: current }, newValues: { status: state }, reason: reason ?? null, deliveryEventId: eventId
      });
      return { id: requestId, state, duplicate: false, approvedBy: request.rows[0]!.approved_by };
    });
    if (!result.duplicate && result.approvedBy) await this.publish(`user:${result.approvedBy}`, { type: `delivery.${action}`, requestId });
    return { id: result.id, state: result.state, duplicate: result.duplicate };
  }

  async remainingQuantities(tx: Queryable, invoiceId: string): Promise<Map<string, number>> {
    const result = await tx.query<{ product_code: string; quantity: string; reserved: string }>(
      `SELECT vi.product_code, vi.quantity,
       COALESCE((SELECT SUM(dri.approved_quantity)
         FROM delivery_request_items dri JOIN delivery_requests dr ON dr.id = dri.delivery_request_id
         WHERE dr.invoice_id = $1 AND dri.product_code = vi.product_code AND dr.status IN ('pending','delivered','returned')), 0) AS reserved
       FROM invoices i JOIN invoice_version_items vi ON vi.invoice_version_id = i.operational_version_id
       WHERE i.id = $1`,
      [invoiceId]
    );
    return new Map(result.rows.map((row) => [row.product_code, Math.max(0, Number(row.quantity) - Number(row.reserved))]));
  }
}
