import { Injectable, Logger } from '@nestjs/common';
import { eq, and, sql } from 'drizzle-orm';
import PdfPrinter from 'pdfmake';
import type { TDocumentDefinitions, Content } from 'pdfmake/interfaces';
import { DbService, Tx } from '@/common/db/db.service';
import { documents, tenants, asns, outboundOrders, shipments, loads, invoices, waves, lpns, skus, storers, inventoryBalances, cartons } from '@/db/schema';
import { StorageService } from '@/common/storage/storage.service';
import { requireRequestContext } from '@/common/context/request-context';
import { NotFoundError } from '@/common/errors/domain-error';
import { assertWarehouseAccess, resolveStorerScope } from '@/common/util';
import { buildDocument, DocumentType } from './templates';
import { buildLpnLabelZpl, buildCartonLabelZpl } from './labels';

export interface GenerateInput {
  type: DocumentType;
  number: string;
  refType: string;
  refId: string;
  data: Record<string, unknown>;
}

/**
 * Generates PDF documents (GRN, RCR, pick list, packing list, delivery note,
 * gate pass, invoice, count sheet) and ZPL labels, stores them and records a
 * documents row. Uses the PDF standard fonts so no font files are needed.
 */
@Injectable()
export class DocumentsService {
  private readonly logger = new Logger(DocumentsService.name);
  private readonly printer = new PdfPrinter({
    Helvetica: { normal: 'Helvetica', bold: 'Helvetica-Bold', italics: 'Helvetica-Oblique', bolditalics: 'Helvetica-BoldOblique' },
  });

  constructor(
    private readonly db: DbService,
    private readonly storage: StorageService,
  ) {}

  async generate(tx: Tx, input: GenerateInput) {
    const ctx = requireRequestContext();
    const tenantId = ctx.tenantId!;
    const [tenant] = await tx.select({ name: tenants.name, branding: tenants.branding, currency: tenants.currency }).from(tenants).where(eq(tenants.id, tenantId)).limit(1);
    const def = buildDocument(input.type, input.number, input.data, { tenantName: tenant?.name ?? '', branding: (tenant?.branding as Record<string, unknown>) ?? {}, currency: tenant?.currency ?? 'KWD' });
    const buffer = await this.render(def);
    const key = this.storage.buildKey(tenantId, `documents/${input.type.toLowerCase()}`, `${input.number}.pdf`);
    const stored = await this.storage.put(key, buffer, 'application/pdf');
    const [row] = await tx
      .insert(documents)
      .values({ tenantId, type: input.type, number: input.number, refType: input.refType, refId: input.refId, mimeType: 'application/pdf', storageKey: stored.key, sizeBytes: stored.sizeBytes, generatedById: ctx.userId })
      .returning();
    return row;
  }

  /** Re-generate on demand (e.g. reprint pick list) without a transaction of the caller. */
  generateStandalone(input: GenerateInput) {
    return this.db.run((tx) => this.generate(tx, input));
  }

  async render(def: TDocumentDefinitions): Promise<Buffer> {
    const doc = this.printer.createPdfKitDocument({ defaultStyle: { font: 'Helvetica', fontSize: 9 }, pageMargins: [36, 36, 36, 40], ...def });
    return new Promise<Buffer>((resolve, reject) => {
      const chunks: Buffer[] = [];
      doc.on('data', (c: Buffer) => chunks.push(c));
      doc.on('end', () => resolve(Buffer.concat(chunks)));
      doc.on('error', reject);
      doc.end();
    });
  }

  list(refType: string, refId: string) {
    return this.db.run(async (tx) => {
      await this.assertAccess(tx, refType, refId);
      return tx.select().from(documents).where(and(eq(documents.refType, refType), eq(documents.refId, refId)));
    });
  }

  /**
   * Documents carry no scope columns; access follows the referenced business
   * document (warehouse access for everyone, storer scope for portal users).
   */
  private async assertAccess(tx: Tx, refType: string, refId: string) {
    const scopedStorer = resolveStorerScope();
    const deny = () => {
      throw new NotFoundError('Document');
    };
    switch (refType) {
      case 'ASN': {
        const [a] = await tx.select({ warehouseId: asns.warehouseId, storerId: asns.storerId }).from(asns).where(eq(asns.id, refId)).limit(1);
        if (!a) deny();
        assertWarehouseAccess(a!.warehouseId);
        if (scopedStorer && a!.storerId !== scopedStorer) deny();
        return;
      }
      case 'ORDER': {
        const [o] = await tx.select({ warehouseId: outboundOrders.warehouseId, storerId: outboundOrders.storerId }).from(outboundOrders).where(eq(outboundOrders.id, refId)).limit(1);
        if (!o) deny();
        assertWarehouseAccess(o!.warehouseId);
        if (scopedStorer && o!.storerId !== scopedStorer) deny();
        return;
      }
      case 'SHIPMENT': {
        const [sh] = await tx.select({ warehouseId: shipments.warehouseId, storerId: outboundOrders.storerId }).from(shipments).innerJoin(outboundOrders, eq(outboundOrders.id, shipments.orderId)).where(eq(shipments.id, refId)).limit(1);
        if (!sh) deny();
        assertWarehouseAccess(sh!.warehouseId);
        if (scopedStorer && sh!.storerId !== scopedStorer) deny();
        return;
      }
      case 'INVOICE': {
        const [inv] = await tx.select({ storerId: invoices.storerId }).from(invoices).where(eq(invoices.id, refId)).limit(1);
        if (!inv) deny();
        if (scopedStorer && inv!.storerId !== scopedStorer) deny();
        return;
      }
      case 'LOAD': {
        const [l] = await tx.select({ warehouseId: loads.warehouseId }).from(loads).where(eq(loads.id, refId)).limit(1);
        if (!l) deny();
        assertWarehouseAccess(l!.warehouseId);
        if (scopedStorer) {
          // a load is storer-visible only when every shipment on it belongs to that storer
          const others = await tx.select({ id: shipments.id }).from(shipments).innerJoin(outboundOrders, eq(outboundOrders.id, shipments.orderId)).where(and(eq(shipments.loadId, refId), sql`${outboundOrders.storerId} <> ${scopedStorer}`)).limit(1);
          if (others.length) deny();
        }
        return;
      }
      case 'WAVE': {
        const [w] = await tx.select({ warehouseId: waves.warehouseId }).from(waves).where(eq(waves.id, refId)).limit(1);
        if (!w) deny();
        assertWarehouseAccess(w!.warehouseId);
        if (scopedStorer) deny(); // pick lists span storers
        return;
      }
      default:
        if (scopedStorer) deny();
    }
  }

  async download(id: string): Promise<{ buffer: Buffer; mimeType: string; fileName: string }> {
    return this.db.run(async (tx) => {
      const [d] = await tx.select().from(documents).where(eq(documents.id, id)).limit(1);
      if (!d) throw new NotFoundError('Document', id);
      await this.assertAccess(tx, d.refType, d.refId);
      const buffer = await this.storage.get(d.storageKey);
      return { buffer, mimeType: d.mimeType, fileName: `${d.number}.${d.mimeType === 'application/pdf' ? 'pdf' : 'txt'}` };
    });
  }

  /** Builds the LPN label from live data: first content line (SKU, lot, expiry, qty) and the storer. */
  lpnLabelFor(lpnNumber: string) {
    return this.db.run(async (tx) => {
      const [lpn] = await tx.select().from(lpns).where(eq(lpns.lpnNumber, lpnNumber)).limit(1);
      if (!lpn) throw new NotFoundError('LPN', lpnNumber);
      assertWarehouseAccess(lpn.warehouseId);
      const scoped = resolveStorerScope();
      if (scoped && lpn.storerId && lpn.storerId !== scoped) throw new NotFoundError('LPN', lpnNumber);
      const contents = await tx
        .select({ skuCode: skus.code, description: skus.description, qty: sql<string>`sum(${inventoryBalances.qtyOnHand})`, lot: inventoryBalances.lotNumber, expiry: inventoryBalances.expiryDate, storerName: storers.name })
        .from(inventoryBalances)
        .innerJoin(skus, eq(skus.id, inventoryBalances.skuId))
        .innerJoin(storers, eq(storers.id, inventoryBalances.storerId))
        .where(and(eq(inventoryBalances.lpnId, lpn.id), sql`${inventoryBalances.qtyOnHand} > 0`))
        .groupBy(skus.code, skus.description, inventoryBalances.lotNumber, inventoryBalances.expiryDate, storers.name);
      const first = contents[0];
      return { zpl: buildLpnLabelZpl({ lpnNumber, storerName: first?.storerName, skuCode: contents.length === 1 ? first.skuCode : contents.length ? 'MIXED' : undefined, description: contents.length === 1 ? first.description : undefined, qty: first ? String(contents.reduce((a, c) => a + Number(c.qty), 0)) : undefined, lot: contents.length === 1 ? first.lot ?? undefined : undefined, expiry: contents.length === 1 ? first.expiry ?? undefined : undefined }), lines: contents.length };
    });
  }

  cartonLabelFor(cartonId: string) {
    return this.db.run(async (tx) => {
      const [row] = await tx
        .select({ carton: cartons, orderNumber: outboundOrders.orderNumber, shipTo: outboundOrders.shipTo, carrierName: outboundOrders.carrierName, storerId: outboundOrders.storerId, warehouseId: shipments.warehouseId, cartonCount: shipments.cartonCount })
        .from(cartons)
        .innerJoin(shipments, eq(shipments.id, cartons.shipmentId))
        .innerJoin(outboundOrders, eq(outboundOrders.id, shipments.orderId))
        .where(eq(cartons.id, cartonId))
        .limit(1);
      if (!row) throw new NotFoundError('Carton', cartonId);
      assertWarehouseAccess(row.warehouseId);
      const scoped = resolveStorerScope();
      if (scoped && row.storerId !== scoped) throw new NotFoundError('Carton', cartonId);
      const shipTo = (row.shipTo ?? {}) as { name?: string; address?: { line1?: string; city?: string; country?: string } };
      const idx = Number(row.carton.cartonNumber.split('-').pop());
      return { zpl: buildCartonLabelZpl({ cartonNumber: row.carton.cartonNumber, orderNumber: row.orderNumber, shipToName: shipTo.name, address: [shipTo.address?.line1, shipTo.address?.city, shipTo.address?.country].filter(Boolean).join(', ') || undefined, cartonIndex: Number.isFinite(idx) ? idx : undefined, cartonCount: row.cartonCount ?? undefined, carrier: row.carrierName ?? undefined, trackingNo: row.carton.trackingNo ?? undefined }) };
    });
  }

  /** ZPL for a pallet label. Returned as text for direct printing from the RF device. */
  lpnLabel(input: { lpnNumber: string; storerName?: string; skuCode?: string; description?: string; qty?: string; lot?: string; expiry?: string; asnNumber?: string }) {
    return buildLpnLabelZpl(input);
  }

  cartonLabel(input: { cartonNumber: string; orderNumber: string; shipToName?: string; address?: string; cartonIndex?: number; cartonCount?: number; carrier?: string; trackingNo?: string }) {
    return buildCartonLabelZpl(input);
  }

  contentOf(c: Content) {
    return c;
  }
}
