/**
 * SBV ISO 20022 Message Validator
 * Validates incoming/outgoing messages against SBV-profiled ISO 20022 XSD schemas
 *
 * Cơ quan chủ quản: Ngân hàng Nhà nước Việt Nam (SBV)
 * Phiên bản: 1.0.0
 */

import Ajv, { ValidateFunction } from 'ajv';
import addFormats from 'ajv-formats';
import { readFileSync } from 'fs';
import { join } from 'path';

// ─── Schema Registry ─────────────────────────────────────────────────────────

const SCHEMA_DIR = join(__dirname, '../message-schema');

/**
 * Tập catalogue các message ISO 20022 được SBV hỗ trợ.
 * Key = Message Definition Identifier (MsgDefIdr trong head.001)
 */
const SUPPORTED_MESSAGES = [
  // pain — Payment Initiation
  'pain.001.001.09',
  'pain.002.001.11',
  'pain.007.001.09',
  'pain.008.001.09',
  'pain.013.001.09',
  // pacs — Payment Clearing & Settlement
  'pacs.002.001.11',
  'pacs.003.001.09',
  'pacs.004.001.10',
  'pacs.008.001.09',
  'pacs.009.001.09',
  'pacs.010.001.05',
  // camt — Cash Management
  'camt.052.001.10',
  'camt.053.001.10',
  'camt.054.001.10',
  'camt.055.001.10',
  'camt.056.001.10',
  'camt.057.001.08',
  'camt.060.001.05',
  // head — Business Application Header
  'head.001.001.03',
  // auth — Regulatory Reporting
  'auth.001.001.02',
  'auth.002.001.02',
  'auth.018.001.04',
] as const;

export type MessageDefId = typeof SUPPORTED_MESSAGES[number];

// ─── Validation Result ───────────────────────────────────────────────────────

export interface ValidationResult {
  valid:    boolean;
  msgDefId: string;
  errors:   ValidationError[];
  warnings: ValidationWarning[];
}

export interface ValidationError {
  field:   string;
  message: string;
  value?:  unknown;
}

export interface ValidationWarning {
  field:   string;
  message: string;
}

// ─── Validator Class ─────────────────────────────────────────────────────────

export class Iso20022Validator {
  private ajv: Ajv;
  private schemaCache = new Map<string, ValidateFunction>();

  constructor() {
    this.ajv = new Ajv({ allErrors: true, strict: false });
    addFormats(this.ajv);
    this.preloadSchemas();
  }

  /**
   * Nạp trước tất cả JSON Schema vào bộ nhớ cache khi khởi động gateway.
   */
  private preloadSchemas(): void {
    for (const msgId of SUPPORTED_MESSAGES) {
      const schemaPath = join(SCHEMA_DIR, `${msgId}.json`);
      try {
        const schema = JSON.parse(readFileSync(schemaPath, 'utf-8'));
        const validate = this.ajv.compile(schema);
        this.schemaCache.set(msgId, validate);
      } catch {
        // Schema file chưa có — bỏ qua, sẽ fallback sang passthrough mode
        console.warn(`[SBV-GW] Schema not found for ${msgId}, running in passthrough mode`);
      }
    }
  }

  /**
   * Xác thực một message ISO 20022 đã được parse sang JavaScript object.
   * @param msgDefId  - Message Definition Identifier (vd: 'pacs.008.001.09')
   * @param payload   - Object đã parse từ XML/JSON
   */
  validate(msgDefId: string, payload: unknown): ValidationResult {
    const errors:   ValidationError[]   = [];
    const warnings: ValidationWarning[] = [];

    // 1. Kiểm tra message có nằm trong danh sách hỗ trợ không
    if (!SUPPORTED_MESSAGES.includes(msgDefId as MessageDefId)) {
      errors.push({ field: 'MsgDefIdr', message: `Message type '${msgDefId}' không được SBV Gateway hỗ trợ` });
      return { valid: false, msgDefId, errors, warnings };
    }

    // 2. Xác thực schema JSON
    const validate = this.schemaCache.get(msgDefId);
    if (validate) {
      const ok = validate(payload);
      if (!ok && validate.errors) {
        for (const e of validate.errors) {
          errors.push({
            field:   e.instancePath || e.schemaPath,
            message: e.message ?? 'Schema validation error',
            value:   e.data,
          });
        }
      }
    }

    // 3. Business rules bổ sung (không thể kiểm tra bằng JSON Schema thuần)
    const businessErrors = this.applyBusinessRules(msgDefId, payload);
    errors.push(...businessErrors);

    // 4. Warnings (không block nhưng cần log)
    const bwarn = this.applyWarningRules(msgDefId, payload);
    warnings.push(...bwarn);

    return { valid: errors.length === 0, msgDefId, errors, warnings };
  }

  /**
   * Business rules SBV-specific không thể express bằng JSON Schema.
   */
  private applyBusinessRules(msgDefId: string, payload: unknown): ValidationError[] {
    const errors: ValidationError[] = [];
    const p = payload as Record<string, unknown>;

    // Rule BR-001: CtrlSum phải khớp tổng InstdAmt trong tất cả transactions
    if (msgDefId.startsWith('pain.001') || msgDefId.startsWith('pacs.008')) {
      try {
        const doc = (p as Record<string, Record<string, unknown>>).Document;
        const rawInitn: unknown = (doc.CstmrCdtTrfInitn ?? doc.FIToFICstmrCdtTrf);
        if (rawInitn && typeof rawInitn === 'object') {
          const initn    = rawInitn as Record<string, unknown>;
          const grpHdr   = initn.GrpHdr as Record<string, unknown>;
          const ctrlSum  = parseFloat(grpHdr.CtrlSum as string);
          const pmtInfArr = (initn.PmtInf ?? initn.CdtTrfTxInf) as unknown[];
          let calcSum = 0;
          if (Array.isArray(pmtInfArr)) {
            for (const pmtInf of pmtInfArr) {
              const pi = pmtInf as Record<string, unknown>;
              const cs = parseFloat(pi.CtrlSum as string);
              if (!isNaN(cs)) calcSum += cs;
            }
            if (Math.abs(ctrlSum - calcSum) > 0.01) {
              errors.push({ field: 'GrpHdr/CtrlSum', message: `CtrlSum (${ctrlSum}) không khớp tổng transactions (${calcSum})` });
            }
          }
        }
      } catch { /* bỏ qua lỗi parse lúc validate */ }
    }

    // Rule BR-002: UETR phải là UUID v4 hợp lệ
    const uetrPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
    try {
      const uetr = extractUETR(p);
      if (uetr && !uetrPattern.test(uetr)) {
        errors.push({ field: 'PmtId/UETR', message: `UETR '${uetr}' không đúng định dạng UUID v4`, value: uetr });
      }
    } catch { /* trường UETR có thể không có trong tất cả message types */ }

    // Rule BR-003: Tiền tệ phải là VND hoặc ngoại tệ được NHNN chấp thuận
    const allowedCurrencies = new Set(['VND', 'USD', 'EUR', 'CNY', 'JPY', 'KRW', 'SGD', 'THB']);
    try {
      const ccy = extractCurrency(p);
      if (ccy && !allowedCurrencies.has(ccy)) {
        errors.push({ field: 'Ccy', message: `Tiền tệ '${ccy}' không được Ngân hàng Nhà nước chấp thuận`, value: ccy });
      }
    } catch { /* */ }

    return errors;
  }

  /**
   * Cảnh báo không block giao dịch nhưng cần ghi log để giám sát.
   */
  private applyWarningRules(_msgDefId: string, payload: unknown): ValidationWarning[] {
    const warnings: ValidationWarning[] = [];
    const p = payload as Record<string, unknown>;

    // Warning W-001: Giao dịch giá trị lớn (> 500 triệu VND) → kích hoạt AML review
    try {
      const amount = extractAmount(p);
      if (amount > 500_000_000) {
        warnings.push({ field: 'IntrBkSttlmAmt', message: `Giao dịch lớn (${amount.toLocaleString('vi-VN')} VND) — AML enhanced due diligence required` });
      }
    } catch { /* */ }

    // Warning W-002: LEI không có trong message
    try {
      const hasLEI = checkLEIPresence(p);
      if (!hasLEI) {
        warnings.push({ field: 'FinInstnId/LEI', message: 'LEI không có — khuyến nghị bổ sung để đạt chuẩn mBridge' });
      }
    } catch { /* */ }

    return warnings;
  }
}

// ─── Helpers ─────────────────────────────────────────────────────────────────

function extractUETR(p: Record<string, unknown>): string | null {
  try {
    const doc   = (p as Record<string, Record<string, unknown>>).Document;
    const root  = Object.values(doc)[0] as Record<string, unknown>;
    const txInf = (Array.isArray(root.CdtTrfTxInf) ? root.CdtTrfTxInf[0] : root.CdtTrfTxInf) as Record<string, unknown>;
    return ((txInf.PmtId as Record<string, string>)?.UETR) ?? null;
  } catch { return null; }
}

function extractCurrency(p: Record<string, unknown>): string | null {
  try {
    const doc   = (p as Record<string, Record<string, unknown>>).Document;
    const root  = Object.values(doc)[0] as Record<string, unknown>;
    const txInf = (Array.isArray(root.CdtTrfTxInf) ? root.CdtTrfTxInf[0] : root.CdtTrfTxInf) as Record<string, unknown>;
    const amt   = txInf.IntrBkSttlmAmt as Record<string, unknown> | undefined;
    const attrs = amt?.['$'] as Record<string, string> | undefined;
    return attrs?.Ccy ?? null;
  } catch { return null; }
}

function extractAmount(p: Record<string, unknown>): number {
  const doc   = (p as Record<string, Record<string, unknown>>).Document;
  const root  = Object.values(doc)[0] as Record<string, unknown>;
  const txInf = (Array.isArray(root.CdtTrfTxInf) ? root.CdtTrfTxInf[0] : root.CdtTrfTxInf) as Record<string, unknown>;
  const amt   = (txInf.IntrBkSttlmAmt as Record<string, string>)?._;
  return parseFloat(amt ?? '0');
}

function checkLEIPresence(p: Record<string, unknown>): boolean {
  const str = JSON.stringify(p);
  return str.includes('"LEI"') || str.includes('"lei"');
}
