import { readFile } from 'node:fs/promises';
import path from 'node:path';
import type { Font, Template } from '@pdfme/common';
import { generate } from '@pdfme/generator';
import { image, line, table, text } from '@pdfme/schemas';
import { eq } from 'drizzle-orm';
import { crmQuotationAttachments, crmQuotations, organizations } from '@/db/schema';
import { db } from '@/lib/db';
import { AuthError } from '@/lib/auth/session';
import {
  createArtifact,
  downloadArtifact,
  getActiveArtifactForEntity,
  lockArtifact
} from '@/features/foundation/document-artifact/server/service';
import {
  buildOrganizationStorageKey,
  getStorageProvider
} from '@/features/foundation/storage/service';
import { sha256 } from '@/features/foundation/storage/server/checksum';
import {
  getQuotationDocumentPreviewData,
  prepareApprovedQuotationSnapshot
} from '@/features/crm/quotations/document/server/service';
import type { ApprovedQuotationSnapshot } from '@/features/crm/quotations/document/types';
import { getQuotationDetail } from '@/features/crm/quotations/server/service';

type GeneratePdfFromTemplateInput = {
  template: Template;
  inputs: Array<Record<string, unknown>>;
};

type GeneratedQuotationPdf = {
  buffer: Buffer;
  fileName: string;
  templateVersionId: string;
  approvedPdfUrl?: string | null;
  approvedArtifactId?: string | null;
  isLegacy?: boolean;
};

const TEMPLATE_FONT_FALLBACKS = new Set(['cordia', 'cordiaBold']);

function getPdfPlugins() {
  return {
    text,
    image,
    line,
    table
  };
}

async function resolvePdfFonts(): Promise<Font> {
  const regularPath = path.join(process.cwd(), 'public', 'fonts', 'cordia_new_r.ttf');
  const boldPath = path.join(process.cwd(), 'public', 'fonts', 'Cordia_New_Bold.ttf');

  try {
    const [regularFontData, boldFontData] = await Promise.all([
      readFile(regularPath),
      readFile(boldPath)
    ]);

    return {
      cordia: {
        data: new Uint8Array(regularFontData),
        fallback: true
      },
      cordiaBold: {
        data: new Uint8Array(boldFontData)
      }
    };
  } catch {
    return {};
  }
}

function normalizeTemplateFonts<T>(value: T): T {
  if (Array.isArray(value)) {
    return value.map((item) => normalizeTemplateFonts(item)) as T;
  }

  if (value && typeof value === 'object') {
    const nextObject = Object.fromEntries(
      Object.entries(value as Record<string, unknown>).map(([key, child]) => {
        if (key === 'fontName' && typeof child === 'string' && TEMPLATE_FONT_FALLBACKS.has(child)) {
          return [key, 'Roboto'];
        }

        return [key, normalizeTemplateFonts(child)];
      })
    );

    return nextObject as T;
  }

  return value;
}

export async function generatePdfFromTemplate(input: GeneratePdfFromTemplateInput) {
  const font = await resolvePdfFonts();
  const generatorInput = {
    template: input.template,
    inputs: input.inputs,
    plugins: getPdfPlugins(),
    options: Object.keys(font).length > 0 ? { font } : undefined
  };
  let pdf: Uint8Array;

  try {
    pdf = await generate(generatorInput);
  } catch (error) {
    if (
      error instanceof Error &&
      (error.message.includes('Font "') || error.message.includes('fontKitFont.layout'))
    ) {
      try {
        pdf = await generate({
          ...generatorInput,
          template: normalizeTemplateFonts(input.template),
          options: undefined
        });
      } catch {
        throw new AuthError('Unable to generate PDF from the current template', 500);
      }
    } else {
      throw new AuthError('Unable to generate PDF from the current template', 500);
    }
  }

  return Buffer.from(pdf);
}

function toSafeFileName(value: string) {
  return value.replace(/[^a-zA-Z0-9._-]/g, '-');
}

async function persistApprovedQuotationArtifact(args: {
  quotationId: string;
  organizationId: string;
  userId: string;
  fileName: string;
  buffer: Buffer;
  templateVersionId: string;
}) {
  const quotation = await getQuotationDetail(args.quotationId, args.organizationId);

  if (quotation.approvedArtifactId && quotation.approvedArtifact?.status === 'locked') {
    throw new AuthError(
      'This quotation already has a locked approved PDF artifact and cannot be overwritten',
      409
    );
  }

  const existingArtifact = await getActiveArtifactForEntity({
    organizationId: args.organizationId,
    entityType: 'crm_quotation',
    entityId: args.quotationId,
    artifactType: 'approved_pdf'
  });

  if (existingArtifact?.status === 'locked') {
    throw new AuthError(
      'This quotation already has a locked approved PDF artifact and cannot be overwritten',
      409
    );
  }

  if (existingArtifact) {
    throw new AuthError(
      'An approved PDF artifact already exists for this quotation. Void it first before regenerating.',
      409
    );
  }

  const snapshot = await prepareApprovedQuotationSnapshot(
    args.quotationId,
    args.organizationId,
    args.userId
  );
  const storageProvider = getStorageProvider();
  const safeFileName = toSafeFileName(args.fileName);
  const checksum = sha256(args.buffer);
  const storageKey = buildOrganizationStorageKey(args.organizationId, [
    'quotations',
    'approved',
    `${args.quotationId}-${safeFileName}`
  ]);

  const stored = await storageProvider.putObject({
    key: storageKey,
    body: args.buffer,
    contentType: 'application/pdf',
    fileName: safeFileName
  });
  const artifact = await createArtifact({
    organizationId: args.organizationId,
    userId: args.userId,
    entityType: 'crm_quotation',
    entityId: args.quotationId,
    documentType: 'quotation_pdf',
    artifactType: 'approved_pdf',
    templateVersionId: args.templateVersionId,
    storageProvider: stored.provider,
    storageKey: stored.key,
    fileName: safeFileName,
    contentType: 'application/pdf',
    fileSize: stored.size,
    checksum,
    metadata: {
      quotationCode: quotation.code,
      source: 'task-i-storage-provider'
    }
  });
  const lockedArtifact = await lockArtifact({
    artifactId: artifact.id,
    organizationId: args.organizationId,
    userId: args.userId
  });
  const artifactReference = `artifact:${lockedArtifact.id}`;

  await db
    .update(crmQuotations)
    .set({
      approvedAt: quotation.approvedAt ? new Date(quotation.approvedAt) : new Date(),
      approvedArtifactId: lockedArtifact.id,
      approvedPdfUrl: artifactReference,
      approvedSnapshot: snapshot,
      approvedTemplateVersionId: args.templateVersionId,
      updatedAt: new Date(),
      updatedBy: args.userId
    })
    .where(eq(crmQuotations.id, args.quotationId));

  const existingAttachment = await db.query.crmQuotationAttachments.findFirst({
    where: eq(crmQuotationAttachments.quotationId, args.quotationId)
  });

  if (existingAttachment) {
    await db
      .update(crmQuotationAttachments)
      .set({
        fileName: safeFileName,
        originalFileName: safeFileName,
        filePath: artifactReference,
        fileSize: stored.size,
        fileType: 'application/pdf',
        description: `Approved quotation PDF artifact ${lockedArtifact.id}`,
        uploadedAt: new Date(),
        uploadedBy: args.userId,
        updatedAt: new Date()
      })
      .where(eq(crmQuotationAttachments.id, existingAttachment.id));
  } else {
    await db.insert(crmQuotationAttachments).values({
      id: crypto.randomUUID(),
      organizationId: args.organizationId,
      quotationId: args.quotationId,
      fileName: safeFileName,
      originalFileName: safeFileName,
      filePath: artifactReference,
      fileSize: stored.size,
      fileType: 'application/pdf',
      description: `Approved quotation PDF artifact ${lockedArtifact.id}`,
      uploadedBy: args.userId
    });
  }

  return {
    artifactId: lockedArtifact.id,
    artifactReference,
    snapshot
  };
}

async function loadLegacyApprovedQuotationPdf(
  quotationId: string,
  organizationId: string
): Promise<GeneratedQuotationPdf> {
  const quotation = await getQuotationDetail(quotationId, organizationId);

  if (!quotation.approvedPdfUrl || !quotation.approvedPdfUrl.startsWith('/generated/')) {
    throw new AuthError('Approved PDF has not been generated yet', 404);
  }

  const relativePath = quotation.approvedPdfUrl.replace(/^\//, '');
  const absolutePath = path.join(process.cwd(), 'public', relativePath);
  const buffer = await readFile(absolutePath).catch(() => {
    throw new AuthError('Approved PDF file is missing from legacy local storage', 404);
  });

  return {
    buffer,
    fileName: `${quotation.code}-approved.pdf`,
    templateVersionId: quotation.approvedTemplateVersionId ?? '',
    approvedPdfUrl: quotation.approvedPdfUrl,
    isLegacy: true
  };
}

export async function generateQuotationPdf(
  quotationId: string,
  organizationId: string
): Promise<GeneratedQuotationPdf> {
  const preview = await getQuotationDocumentPreviewData(quotationId, organizationId);
  const buffer = await generatePdfFromTemplate({
    template: preview.template.version.schemaJson as Template,
    inputs: [preview.templateInput]
  });

  return {
    buffer,
    fileName: `${preview.documentData.quotation.code}.pdf`,
    templateVersionId: preview.template.version.id
  };
}

export async function generateQuotationPreviewPdf(quotationId: string, organizationId: string) {
  return generateQuotationPdf(quotationId, organizationId);
}

export async function generateApprovedQuotationPdf(
  quotationId: string,
  organizationId: string,
  userId: string
): Promise<GeneratedQuotationPdf> {
  const preview = await getQuotationDocumentPreviewData(quotationId, organizationId);
  const quotation = await getQuotationDetail(quotationId, organizationId);
  const approvedSnapshot = quotation.approvedSnapshot as ApprovedQuotationSnapshot | null;

  if (preview.documentData.quotation.statusCode !== 'approved') {
    throw new AuthError('Only approved quotations can generate approved PDF', 400);
  }

  const snapshotTemplateInput =
    approvedSnapshot &&
    approvedSnapshot.quotationId === quotationId &&
    approvedSnapshot.templateInput &&
    typeof approvedSnapshot.templateInput === 'object'
      ? approvedSnapshot.templateInput
      : null;
  const buffer = await generatePdfFromTemplate({
    template: preview.template.version.schemaJson as Template,
    inputs: [snapshotTemplateInput ?? preview.templateInput]
  });
  const fileName = `${preview.documentData.quotation.code}-approved.pdf`;
  const artifact = await persistApprovedQuotationArtifact({
    quotationId,
    organizationId,
    userId,
    fileName,
    buffer,
    templateVersionId: preview.template.version.id
  });

  return {
    buffer,
    fileName,
    templateVersionId: preview.template.version.id,
    approvedPdfUrl: artifact.artifactReference,
    approvedArtifactId: artifact.artifactId
  };
}

export async function getStoredApprovedQuotationPdf(
  quotationId: string,
  organizationId: string,
  options?: {
    userId?: string;
    auditDownload?: boolean;
  }
): Promise<GeneratedQuotationPdf> {
  const quotation = await getQuotationDetail(quotationId, organizationId);
  const approvedArtifactId =
    quotation.approvedArtifactId ??
    (quotation.approvedPdfUrl?.startsWith('artifact:')
      ? quotation.approvedPdfUrl.replace('artifact:', '')
      : null);

  if (approvedArtifactId) {
    const { object } = await downloadArtifact({
      artifactId: approvedArtifactId,
      organizationId,
      userId: options?.userId,
      auditDownload: options?.auditDownload
    });
    const response = new Response(object.body);
    const buffer = Buffer.from(await response.arrayBuffer());

    return {
      buffer,
      fileName: object.fileName || `${quotation.code}-approved.pdf`,
      templateVersionId: quotation.approvedTemplateVersionId ?? '',
      approvedPdfUrl: quotation.approvedPdfUrl,
      approvedArtifactId
    };
  }

  return loadLegacyApprovedQuotationPdf(quotationId, organizationId);
}

export async function getOrganizationGeneratedPdfDirectory(organizationId: string) {
  const [organization] = await db
    .select()
    .from(organizations)
    .where(eq(organizations.id, organizationId))
    .limit(1);

  if (!organization) {
    throw new AuthError('Organization not found', 404);
  }

  return buildOrganizationStorageKey(organizationId, ['quotations']);
}
