import { writeFile } from 'node:fs/promises';
import path from 'node:path';
import { and, isNull } from 'drizzle-orm';
import { crmQuotations } from '../src/db/schema';
import { db } from '../src/lib/db';
import { getQuotationDocumentPreviewData } from '../src/features/crm/quotations/document/server/service';
import {
  resolveTemplateForDocument,
  resolveTemplateMappings
} from '../src/features/foundation/document-template/server/service';

type AuditIssue =
  | 'NO_MAPPING'
  | 'NO_TEMPLATE_FIELD'
  | 'RESOLVED_UNDEFINED'
  | 'RESOLVED_NULL'
  | 'TABLE_TYPE_MISMATCH'
  | 'TOPIC_MAPPING_MISMATCH';

type AuditClassification =
  | 'static_field'
  | 'dynamic_template'
  | 'legacy_content_token'
  | 'runtime_dynamic_input';

interface AuditResult {
  placeholderKey: string;
  templateExists: boolean;
  mappingExists: boolean;
  sourcePath: string | null;
  resolvedValue: unknown;
  resolvedType: string;
  classification: AuditClassification;
  dynamicTemplate: boolean;
  legacyContentToken: boolean;
  runtimeGenerated: boolean;
  issues: AuditIssue[];
  notes: string[];
}

const STATIC_EXPECTED_KEYS = new Set([
  'customer_name',
  'customer_addr',
  'customer_tel',
  'customer_email',
  'customer_att',
  'project_name',
  'site_location',
  'quotation_date',
  'quotation_code',
  'quotation_price',
  'currency',
  'exclusion_data',
  'app1',
  'app1_position',
  'app2',
  'app2_position',
  'app3',
  'app3_position'
]);

const DYNAMIC_TEMPLATE_NAMES = new Set(['topic', 'data_topic']);
const LEGACY_CONTENT_TOKENS = new Set(['item_topic']);

function extractSchemaInventory(template: unknown) {
  const fieldNames = new Set<string>();
  const contentTokens = new Set<string>();

  if (!template || typeof template !== 'object' || !('schemas' in template)) {
    return { fieldNames, contentTokens };
  }

  const pages = (template as { schemas?: unknown }).schemas;

  if (!Array.isArray(pages)) {
    return { fieldNames, contentTokens };
  }

  for (const page of pages) {
    if (!Array.isArray(page)) {
      continue;
    }

    for (const field of page) {
      if (!field || typeof field !== 'object') {
        continue;
      }

      if ('name' in field && typeof field.name === 'string') {
        fieldNames.add(field.name);
      }

      for (const token of extractTokensFromValue((field as { content?: unknown }).content)) {
        contentTokens.add(token);
      }

      const head = (field as { head?: unknown }).head;

      if (Array.isArray(head)) {
        for (const value of head) {
          for (const token of extractTokensFromValue(value)) {
            contentTokens.add(token);
          }
        }
      }
    }
  }

  return { fieldNames, contentTokens };
}

function extractTokensFromValue(value: unknown) {
  const tokens = new Set<string>();

  if (typeof value !== 'string') {
    return tokens;
  }

  for (const match of value.matchAll(/\{([^}]+)\}/g)) {
    tokens.add(match[1]);
  }

  try {
    const parsed = JSON.parse(value) as unknown;

    if (Array.isArray(parsed)) {
      for (const row of parsed) {
        if (!Array.isArray(row)) {
          continue;
        }

        for (const cell of row) {
          if (typeof cell !== 'string') {
            continue;
          }

          for (const match of cell.matchAll(/\{([^}]+)\}/g)) {
            tokens.add(match[1]);
          }
        }
      }
    }
  } catch {
    return tokens;
  }

  return tokens;
}

function classifyKey(key: string): AuditClassification {
  if (DYNAMIC_TEMPLATE_NAMES.has(key)) {
    return 'dynamic_template';
  }

  if (LEGACY_CONTENT_TOKENS.has(key)) {
    return 'legacy_content_token';
  }

  if (key.startsWith('topic_') || key.startsWith('item_topic_')) {
    return 'runtime_dynamic_input';
  }

  return 'static_field';
}

async function findTestQuotation() {
  const quotations = await db
    .select()
    .from(crmQuotations)
    .where(and(isNull(crmQuotations.deletedAt)))
    .limit(5);

  if (quotations.length === 0) {
    throw new Error('No quotations found in database');
  }

  return quotations[0];
}

async function auditPdfmeRuntime() {
  console.log('=== PDFME Runtime Audit ===\n');

  console.log('Step 1: Finding test quotation...');
  const quotation = await findTestQuotation();
  console.log(`  Found quotation: ${quotation.code} (${quotation.id})`);

  console.log('\nStep 2: Getting base template and mappings...');
  const template = await resolveTemplateForDocument(quotation.organizationId, {
    documentType: 'quotation',
    productType: 'default',
    fileType: 'pdfme'
  });
  const mappings = await resolveTemplateMappings(template.version.id, quotation.organizationId);
  console.log(`  Template: ${template.template.templateName} (${template.version.id})`);
  console.log(`  Mappings found: ${mappings.length}`);

  console.log('\nStep 3: Building preview runtime data...');
  const preview = await getQuotationDocumentPreviewData(
    quotation.id,
    quotation.organizationId
  );
  const templateInput = preview.templateInput;
  console.log('  Preview runtime data built successfully');

  console.log('\nStep 4: Extracting schema inventory from base template...');
  const inventory = extractSchemaInventory(template.version.schemaJson);
  const auditedKeys = new Set<string>([
    ...STATIC_EXPECTED_KEYS,
    ...inventory.fieldNames,
    ...inventory.contentTokens,
    ...Object.keys(templateInput).filter(
      (key) => key.startsWith('topic_') || key.startsWith('item_topic_')
    )
  ]);
  console.log(`  Audited keys found: ${auditedKeys.size}`);

  console.log('\nStep 5: Performing audit...\n');
  const mappingMap = new Map<string, (typeof mappings)[number]>();
  mappings.forEach((mapping) => mappingMap.set(mapping.placeholderKey, mapping));

  const auditResults: AuditResult[] = [];

  for (const key of auditedKeys) {
    const classification = classifyKey(key);
    const mappingExists = mappingMap.has(key);
    const templateExists = inventory.fieldNames.has(key) || inventory.contentTokens.has(key);
    const result: AuditResult = {
      placeholderKey: key,
      templateExists,
      mappingExists,
      sourcePath: mappingMap.get(key)?.sourcePath ?? null,
      resolvedValue: templateInput[key],
      resolvedType: typeof templateInput[key],
      classification,
      dynamicTemplate: classification === 'dynamic_template',
      legacyContentToken: classification === 'legacy_content_token',
      runtimeGenerated: classification === 'runtime_dynamic_input',
      issues: [],
      notes: []
    };

    if (classification === 'static_field' && templateExists && !mappingExists) {
      result.issues.push('NO_MAPPING');
      result.notes.push('Static schema field exists in template but has no DB mapping');
    }

    if (classification === 'static_field' && mappingExists && !templateExists) {
      result.issues.push('NO_TEMPLATE_FIELD');
      result.notes.push('DB mapping exists but no corresponding schema field or content token was found');
    }

    if (classification === 'static_field' && result.resolvedValue === undefined) {
      result.issues.push('RESOLVED_UNDEFINED');
      result.notes.push('Resolved runtime value is undefined');
    }

    if (classification === 'static_field' && result.resolvedValue === null) {
      result.issues.push('RESOLVED_NULL');
      result.notes.push('Resolved runtime value is null');
    }

    const mapping = mappingMap.get(key);

    if (mapping?.dataType === 'table') {
      const value = result.resolvedValue;

      if (value && !Array.isArray(value)) {
        result.issues.push('TABLE_TYPE_MISMATCH');
        result.notes.push(`Expected string[][] for table mapping, got ${typeof value}`);
      } else if (
        Array.isArray(value) &&
        value.length > 0 &&
        !Array.isArray(value[0])
      ) {
        result.issues.push('TABLE_TYPE_MISMATCH');
        result.notes.push('Expected string[][] for table mapping, got a flat array');
      }
    }

    if (classification === 'dynamic_template') {
      if (mappingExists) {
        result.issues.push('TOPIC_MAPPING_MISMATCH');
        result.notes.push('Dynamic schema template must not have a static DB mapping');
      } else {
        result.notes.push('Handled by Dynamic Topic Engine schema cloning');
      }
    }

    if (classification === 'legacy_content_token') {
      if (mappingExists) {
        result.issues.push('TOPIC_MAPPING_MISMATCH');
        result.notes.push('Legacy item_topic token must not persist as a DB mapping');
      } else {
        result.notes.push('Injected only through dynamic topic runtime inputs');
      }
    }

    if (classification === 'runtime_dynamic_input') {
      if (mappingExists) {
        result.issues.push('TOPIC_MAPPING_MISMATCH');
        result.notes.push('Runtime dynamic topic keys must not be stored in DB mappings');
      } else if (result.resolvedValue !== undefined) {
        result.notes.push('Generated at runtime by Dynamic Topic Engine');
      }
    }

    auditResults.push(result);
  }

  console.log('## Summary');
  console.log(
    `- Static template placeholders: ${[...inventory.fieldNames].filter((key) => STATIC_EXPECTED_KEYS.has(key)).length}`
  );
  console.log(`- Template mappings: ${mappings.length}`);
  console.log(
    `- Dynamic topic runtime keys: ${auditResults.filter((item) => item.runtimeGenerated).length}`
  );
  console.log(`- Issues found: ${auditResults.filter((item) => item.issues.length > 0).length}`);
  console.log('');

  auditResults.sort((left, right) => {
    if (left.issues.length > 0 && right.issues.length === 0) {
      return -1;
    }

    if (left.issues.length === 0 && right.issues.length > 0) {
      return 1;
    }

    return left.placeholderKey.localeCompare(right.placeholderKey);
  });

  console.log('## Detailed Results\n');

  for (const result of auditResults) {
    console.log(`### ${result.placeholderKey}`);
    console.log(`- Template: ${result.templateExists ? 'yes' : 'no'}`);
    console.log(`- Mapping: ${result.mappingExists ? 'yes' : 'no'}`);
    console.log(`- Classification: ${result.classification}`);
    console.log(`- Source: ${result.sourcePath ?? '-'}`);
    console.log(
      `- Value: ${JSON.stringify(result.resolvedValue).slice(0, 120)}${JSON.stringify(result.resolvedValue).length > 120 ? '...' : ''}`
    );
    console.log(`- Type: ${result.resolvedType}`);

    if (result.issues.length > 0) {
      console.log(`- Issues: ${result.issues.join(', ')}`);
    }

    if (result.notes.length > 0) {
      console.log(`- Notes: ${result.notes.join('; ')}`);
    }

    console.log('');
  }

  const outputPath = path.join(
    process.cwd(),
    'docs',
    'implementation',
    'pdfme-runtime-audit-data.json'
  );

  await writeFile(
    outputPath,
    JSON.stringify(
      {
        quotation: {
          id: quotation.id,
          code: quotation.code,
          organizationId: quotation.organizationId
        },
        template: {
          id: template.template.id,
          templateName: template.template.templateName,
          versionId: template.version.id,
          version: template.version.version
        },
        schemaInventory: {
          fieldNames: [...inventory.fieldNames].sort(),
          contentTokens: [...inventory.contentTokens].sort()
        },
        runtimeDynamicKeys: Object.keys(templateInput)
          .filter((key) => key.startsWith('topic_') || key.startsWith('item_topic_'))
          .sort(),
        results: auditResults,
        documentData: preview.documentData,
        templateInput
      },
      null,
      2
    )
  );

  console.log(`Full audit data saved to: ${outputPath}`);
}

auditPdfmeRuntime().catch((error) => {
  console.error('Audit failed:', error);
  process.exit(1);
});
