import { and, desc, eq, inArray, isNull } from 'drizzle-orm';
import { crmDocumentLibraries, crmDocumentLibraryVersions } from '@/db/schema';
import { db } from '@/lib/db';
import { AuthError } from '@/lib/auth/session';
import { auditAction } from '@/features/foundation/audit-log/service';
import {
  createArtifact,
  downloadArtifact,
  getActiveArtifactForEntity,
  voidArtifact,
} from '@/features/foundation/document-artifact/server/service';
import {
  type DocumentLibraryBrand,
  type DocumentLibraryProductType,
  type DocumentLibraryType,
} from '@/features/foundation/document-library/types';
import {
  buildOrganizationStorageKey,
  getStorageProvider,
} from '@/features/foundation/storage/service';
import { sha256 } from '@/features/foundation/storage/server/checksum';
import {
  buildQuotationDocumentData,
  getQuotationDocumentPreviewData,
} from '@/features/crm/quotations/document/server/service';
import { getStoredApprovedQuotationPdf } from '@/features/foundation/pdf-generator/server/service';
import type {
  AssemblyPolicy,
  AssemblySourcePolicy,
  AssemblyWarning,
  DocumentAssemblyResult,
  ResolvedAssemblySource,
} from '../types';
import {
  getPdfPageCount,
  mergePdfBuffers,
  normalizeAssemblyBrand,
  normalizeAssemblyProductType,
  validateAssemblySourceOrdering,
} from './helpers';

type ResolvedDocumentLibraryCandidate = {
  library: typeof crmDocumentLibraries.$inferSelect;
  version: typeof crmDocumentLibraryVersions.$inferSelect;
};

type AssembleDocumentInput = {
  organizationId: string;
  userId: string;
  entityType: string;
  entityId: string;
  fileName: string;
  artifactType: 'assembled_pdf';
  policy: AssemblyPolicy;
  sourceResolver: (
    source: AssemblySourcePolicy,
  ) => Promise<ResolvedAssemblySource | null>;
  auditEntityType?: string;
};

type QuotationAssemblyMetadata = {
  documentType: string;
  sourceDocumentType: string;
  sourceDocumentId: string;
  sourceDocumentCode: string;
  assemblyPolicy: AssemblyPolicy;
  sourceArtifacts: Array<Record<string, unknown>>;
  warnings: AssemblyWarning[];
  pageCount: number;
  generatedAt: string;
  generatedBy: string;
};

const ASSEMBLED_PDF_ARTIFACT_TYPE = 'assembled_pdf';
const DEFAULT_ASSEMBLY_LANGUAGE = 'th';

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

function toLibraryDocumentLabel(documentType: DocumentLibraryType) {
  return documentType.replace(/_/g, ' ').toUpperCase();
}

function toRoleLabel(role: string) {
  return role.replace(/_/g, ' ').toUpperCase();
}

function buildDocumentLibraryMissingMessage(input: {
  role: string;
  documentType: DocumentLibraryType;
  brand: DocumentLibraryBrand;
  language: string;
  productType: DocumentLibraryProductType;
}) {
  return `Missing ${toRoleLabel(input.role)} document (${toLibraryDocumentLabel(
    input.documentType,
  )}, brand ${input.brand}, language ${input.language}, product type ${input.productType}).`;
}

function getCandidateScore(input: {
  candidate: typeof crmDocumentLibraries.$inferSelect;
  brand: DocumentLibraryBrand;
  productType: DocumentLibraryProductType;
}) {
  let score = 0;

  if (input.candidate.brand === input.brand) {
    score += 2;
  }

  if (input.candidate.productType === input.productType) {
    score += 1;
  }

  return score;
}

export async function resolveActiveDocumentLibraryCandidate(input: {
  organizationId: string;
  documentType: DocumentLibraryType;
  brand: DocumentLibraryBrand;
  language: string;
  productType: DocumentLibraryProductType;
}): Promise<ResolvedDocumentLibraryCandidate | null> {
  const brandScope =
    input.brand === 'generic' ? ['generic'] : [input.brand, 'generic'];
  const productTypeScope =
    input.productType === 'all' ? ['all'] : [input.productType, 'all'];

  const rows = await db
    .select({
      library: crmDocumentLibraries,
      version: crmDocumentLibraryVersions,
    })
    .from(crmDocumentLibraryVersions)
    .innerJoin(
      crmDocumentLibraries,
      eq(crmDocumentLibraryVersions.libraryId, crmDocumentLibraries.id),
    )
    .where(
      and(
        eq(crmDocumentLibraries.organizationId, input.organizationId),
        eq(crmDocumentLibraryVersions.organizationId, input.organizationId),
        eq(crmDocumentLibraries.documentType, input.documentType),
        eq(crmDocumentLibraries.language, input.language),
        inArray(crmDocumentLibraries.brand, brandScope),
        inArray(crmDocumentLibraries.productType, productTypeScope),
        eq(crmDocumentLibraries.isActive, true),
        eq(crmDocumentLibraries.status, 'active'),
        eq(crmDocumentLibraryVersions.isActive, true),
        eq(crmDocumentLibraryVersions.status, 'active'),
        isNull(crmDocumentLibraries.deletedAt),
        isNull(crmDocumentLibraryVersions.deletedAt),
      ),
    )
    .orderBy(
      desc(crmDocumentLibraryVersions.createdAt),
      crmDocumentLibraries.code,
    );

  if (!rows.length) {
    return null;
  }

  const [best] = [...rows].sort((left, right) => {
    const scoreDelta =
      getCandidateScore({
        candidate: right.library,
        brand: input.brand,
        productType: input.productType,
      }) -
      getCandidateScore({
        candidate: left.library,
        brand: input.brand,
        productType: input.productType,
      });

    if (scoreDelta !== 0) {
      return scoreDelta;
    }

    const codeDelta = left.library.code.localeCompare(right.library.code);

    if (codeDelta !== 0) {
      return codeDelta;
    }

    return right.version.createdAt.getTime() - left.version.createdAt.getTime();
  });

  return best;
}

async function resolveDocumentLibrarySource(input: {
  organizationId: string;
  role: string;
  required: boolean;
  order: number;
  documentType: DocumentLibraryType;
  brand: DocumentLibraryBrand;
  language: string;
  productType: DocumentLibraryProductType;
}) {
  const candidate = await resolveActiveDocumentLibraryCandidate({
    organizationId: input.organizationId,
    documentType: input.documentType,
    brand: input.brand,
    language: input.language,
    productType: input.productType,
  });

  if (!candidate) {
    return null;
  }

  const object = await getStorageProvider().getObject({
    key: candidate.version.storageKey,
  });
  const buffer = Buffer.from(await new Response(object.body).arrayBuffer());
  const pageCount = await getPdfPageCount(
    buffer,
    `${toRoleLabel(input.role)} source`,
  );

  return {
    role: input.role,
    sourceType: 'document_library' as const,
    required: input.required,
    order: input.order,
    fileName: candidate.version.fileName,
    contentType: candidate.version.mimeType,
    checksum: candidate.version.checksum,
    pageCount,
    fileSize: candidate.version.fileSize,
    metadata: {
      documentType: candidate.library.documentType,
      libraryId: candidate.library.id,
      libraryCode: candidate.library.code,
      libraryName: candidate.library.name,
      brand: candidate.library.brand,
      language: candidate.library.language,
      productType: candidate.library.productType,
      versionId: candidate.version.id,
      version: candidate.version.version,
      storageKey: candidate.version.storageKey,
    },
    buffer,
  };
}

async function persistAssembledArtifact(input: {
  organizationId: string;
  userId: string;
  entityType: string;
  entityId: string;
  artifactType: 'assembled_pdf';
  fileName: string;
  buffer: Buffer;
  checksum: string;
  fileSize: number;
  metadata: QuotationAssemblyMetadata;
}) {
  const existingArtifact = await getActiveArtifactForEntity({
    organizationId: input.organizationId,
    entityType: input.entityType,
    entityId: input.entityId,
    artifactType: input.artifactType,
  });

  if (existingArtifact?.status === 'active') {
    await voidArtifact({
      artifactId: existingArtifact.id,
      organizationId: input.organizationId,
      userId: input.userId,
      reason: 'Superseded by newer assembled PDF artifact.',
    });
  }

  const safeFileName = sanitizeFileName(input.fileName);
  const storageKey = buildOrganizationStorageKey(input.organizationId, [
    'quotations',
    'assembled',
    `${input.entityId}-${safeFileName}`,
  ]);
  const stored = await getStorageProvider().putObject({
    key: storageKey,
    body: input.buffer,
    contentType: 'application/pdf',
    fileName: safeFileName,
  });

  const artifact = await createArtifact({
    organizationId: input.organizationId,
    userId: input.userId,
    entityType: input.entityType,
    entityId: input.entityId,
    documentType: input.metadata.sourceDocumentType,
    artifactType: input.artifactType,
    templateVersionId: null,
    storageProvider: stored.provider,
    storageKey: stored.key,
    fileName: safeFileName,
    contentType: 'application/pdf',
    fileSize: input.fileSize,
    checksum: input.checksum,
    metadata: input.metadata,
  });

  return {
    artifactId: artifact.id,
    storageKey: artifact.storageKey,
    fileName: artifact.fileName,
    checksum: artifact.checksum ?? input.checksum,
    fileSize: artifact.fileSize ?? input.fileSize,
    pageCount: input.metadata.pageCount,
  };
}

export async function assembleDocument(
  input: AssembleDocumentInput,
): Promise<DocumentAssemblyResult> {
  validateAssemblySourceOrdering(input.policy.sources);

  const auditEntityType = input.auditEntityType ?? input.entityType;
  const warnings: AssemblyWarning[] = [];

  await auditAction({
    organizationId: input.organizationId,
    userId: input.userId,
    entityType: auditEntityType,
    entityId: input.entityId,
    action: 'assembly_started',
    afterData: { policy: input.policy },
  });

  try {
    const sources: ResolvedAssemblySource[] = [];

    for (const sourcePolicy of [...input.policy.sources].sort(
      (left, right) => left.order - right.order,
    )) {
      const resolvedSource = await input.sourceResolver(sourcePolicy);

      if (!resolvedSource) {
        if (sourcePolicy.sourceType === 'document_library') {
          const message = buildDocumentLibraryMissingMessage({
            role: sourcePolicy.role,
            documentType: sourcePolicy.documentType!,
            brand: input.policy.brand ?? 'generic',
            language: input.policy.language ?? DEFAULT_ASSEMBLY_LANGUAGE,
            productType: input.policy.productType ?? 'all',
          });

          if (sourcePolicy.required) {
            await auditAction({
              organizationId: input.organizationId,
              userId: input.userId,
              entityType: auditEntityType,
              entityId: input.entityId,
              action: 'assembly_failed',
              afterData: { role: sourcePolicy.role, message },
            });
            throw new AuthError(message, 409);
          }

          warnings.push({
            code: 'missing_optional_source',
            role: sourcePolicy.role,
            message,
          });

          await auditAction({
            organizationId: input.organizationId,
            userId: input.userId,
            entityType: auditEntityType,
            entityId: input.entityId,
            action: 'assembly_source_missing',
            afterData: { role: sourcePolicy.role, message },
          });
          continue;
        }

        throw new AuthError(
          `Required generated source missing for role ${sourcePolicy.role}.`,
          409,
        );
      }

      sources.push(resolvedSource);

      await auditAction({
        organizationId: input.organizationId,
        userId: input.userId,
        entityType: auditEntityType,
        entityId: input.entityId,
        action: 'assembly_source_resolved',
        afterData: {
          role: resolvedSource.role,
          sourceType: resolvedSource.sourceType,
          fileName: resolvedSource.fileName,
          checksum: resolvedSource.checksum,
          pageCount: resolvedSource.pageCount,
          metadata: resolvedSource.metadata,
        },
      });
    }

    if (!sources.length) {
      throw new AuthError('No assembly sources available.', 409);
    }

    const buffer = await mergePdfBuffers(sources);
    const checksum = sha256(buffer);
    const fileSize = buffer.byteLength;
    const pageCount = await getPdfPageCount(buffer, 'Merged assembly output');

    await auditAction({
      organizationId: input.organizationId,
      userId: input.userId,
      entityType: auditEntityType,
      entityId: input.entityId,
      action: 'assembly_merge_completed',
      afterData: {
        fileName: input.fileName,
        checksum,
        fileSize,
        pageCount,
        sourceCount: sources.length,
      },
    });

    return {
      buffer,
      fileName: input.fileName,
      checksum,
      fileSize,
      pageCount,
      policy: input.policy,
      warnings,
      sources,
      artifact: null,
    };
  } catch (error) {
    if (!(error instanceof AuthError)) {
      await auditAction({
        organizationId: input.organizationId,
        userId: input.userId,
        entityType: auditEntityType,
        entityId: input.entityId,
        action: 'assembly_failed',
        afterData: {
          message:
            error instanceof Error
              ? error.message
              : 'Unknown assembly failure',
        },
      });
    }

    throw error;
  }
}

export async function buildDefaultQuotationAssemblyPolicy(input: {
  quotationId: string;
  organizationId: string;
}): Promise<AssemblyPolicy> {
  const documentData = await buildQuotationDocumentData(
    input.quotationId,
    input.organizationId,
  );
  const productTypeCode =
    documentData.items.find((item) => item.productTypeCode)?.productTypeCode ??
    documentData.quotation.quotationTypeCode;

  return {
    documentType: 'quotation_pdf',
    organizationId: input.organizationId,
    brand: normalizeAssemblyBrand(documentData.company.slug),
    language: DEFAULT_ASSEMBLY_LANGUAGE,
    productType: normalizeAssemblyProductType(productTypeCode),
    sources: [
      {
        sourceType: 'generated',
        role: 'main',
        required: true,
        order: 1,
      },
      {
        sourceType: 'document_library',
        role: 'sla',
        required: true,
        order: 2,
        documentType: 'sla',
      },
      {
        sourceType: 'document_library',
        role: 'warranty',
        required: false,
        order: 3,
        documentType: 'warranty',
      },
      {
        sourceType: 'document_library',
        role: 'datasheet',
        required: false,
        order: 4,
        documentType: 'datasheet',
      },
      {
        sourceType: 'document_library',
        role: 'drawing',
        required: false,
        order: 5,
        documentType: 'drawing',
      },
    ],
  };
}

export async function generateAssembledQuotationPdf(
  quotationId: string,
  organizationId: string,
  userId: string,
) {
  const [policy, preview] = await Promise.all([
    buildDefaultQuotationAssemblyPolicy({ quotationId, organizationId }),
    getQuotationDocumentPreviewData(quotationId, organizationId),
  ]);

  const assembled = await assembleDocument({
    organizationId,
    userId,
    entityType: 'crm_quotation',
    entityId: quotationId,
    auditEntityType: 'crm_quotation',
    fileName: `${preview.documentData.quotation.code}-assembled.pdf`,
    artifactType: ASSEMBLED_PDF_ARTIFACT_TYPE,
    policy,
    sourceResolver: async (sourcePolicy) => {
      if (sourcePolicy.sourceType === 'generated') {
        const generated = await getStoredApprovedQuotationPdf(quotationId, organizationId, {
          userId
        });
        const pageCount = await getPdfPageCount(
          generated.buffer,
          'Approved quotation PDF',
        );

        return {
          role: sourcePolicy.role,
          sourceType: 'generated',
          required: sourcePolicy.required,
          order: sourcePolicy.order,
          fileName: generated.fileName,
          contentType: 'application/pdf',
          checksum: sha256(generated.buffer),
          pageCount,
          fileSize: generated.buffer.byteLength,
          metadata: {
            templateVersionId: generated.templateVersionId,
            sourceDocumentType: 'approved_pdf',
          },
          buffer: generated.buffer,
        };
      }

      return resolveDocumentLibrarySource({
        organizationId,
        role: sourcePolicy.role,
        required: sourcePolicy.required,
        order: sourcePolicy.order,
        documentType: sourcePolicy.documentType!,
        brand: policy.brand ?? 'generic',
        language: policy.language ?? DEFAULT_ASSEMBLY_LANGUAGE,
        productType: policy.productType ?? 'all',
      });
    },
  });

  const metadata: QuotationAssemblyMetadata = {
    documentType: 'assembled_pdf',
    sourceDocumentType: 'approved_pdf',
    sourceDocumentId: quotationId,
    sourceDocumentCode: preview.documentData.quotation.code,
    assemblyPolicy: assembled.policy,
    sourceArtifacts: assembled.sources.map((source) => ({
      role: source.role,
      sourceType: source.sourceType,
      fileName: source.fileName,
      checksum: source.checksum,
      fileSize: source.fileSize,
      pageCount: source.pageCount,
      metadata: source.metadata,
    })),
    warnings: assembled.warnings,
    pageCount: assembled.pageCount,
    generatedAt: new Date().toISOString(),
    generatedBy: userId,
  };

  const artifact = await persistAssembledArtifact({
    organizationId,
    userId,
    entityType: 'crm_quotation',
    entityId: quotationId,
    artifactType: ASSEMBLED_PDF_ARTIFACT_TYPE,
    fileName: assembled.fileName,
    buffer: assembled.buffer,
    checksum: assembled.checksum,
    fileSize: assembled.fileSize,
    metadata,
  });

  await auditAction({
    organizationId,
    userId,
    entityType: 'crm_quotation',
    entityId: quotationId,
    action: 'assembly_artifact_stored',
    afterData: artifact,
  });

  return {
    ...assembled,
    artifact,
  };
}

export async function getStoredAssembledQuotationPdf(
  quotationId: string,
  organizationId: string,
  options?: {
    userId?: string;
    auditDownload?: boolean;
  },
) {
  const existingArtifact = await getActiveArtifactForEntity({
    organizationId,
    entityType: 'crm_quotation',
    entityId: quotationId,
    artifactType: ASSEMBLED_PDF_ARTIFACT_TYPE,
  });

  if (!existingArtifact) {
    throw new AuthError('Assembled quotation PDF has not been generated yet.', 404);
  }

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

  return {
    artifact,
    buffer,
    fileName: object.fileName || `${quotationId}-assembled.pdf`,
  };
}
