import stringSimilarity from 'string-similarity';
import { Op, cast, col, where as sqlWhere } from 'sequelize';

/** Shared list/report filters for CPC documents (parity with legacy CPC-CSD). */
export function appendCpcDocumentFilters(
  andParts: Record<string, unknown>[],
  opts: {
    type?: string;
    status?: string;
    search?: string;
    /** When true, `search` also matches document `id` (recent documents API). */
    searchIncludeId?: boolean;
  }
): void {
  const { type, status, search, searchIncludeId = false } = opts;

  if (type && type !== 'ALL') {
    if (type === 'AADHAAR') {
      andParts.push({
        [Op.or]: [
          { documentType: { [Op.iLike]: '%AADHAAR%' } },
          { documentType: { [Op.iLike]: '%ADHAAR%' } }
        ]
      });
    } else if (type === 'RETAIL_INVOICE') {
      andParts.push({
        [Op.or]: [
          { documentType: { [Op.iLike]: '%RETAIL%' } },
          { documentType: { [Op.iLike]: '%INVOICE%' } }
        ]
      });
    } else if (type === 'CPC_AUTH') {
      andParts.push({
        [Op.or]: [
          { documentType: { [Op.iLike]: '%AUTHORITY%' } },
          { documentType: { [Op.iLike]: '%CPC_AUTH%' } },
          { documentType: { [Op.iLike]: '%AUTH%' } }
        ]
      });
    } else if (type === 'CSD_PO') {
      andParts.push({
        [Op.or]: [
          { documentType: { [Op.iLike]: '%CSD_PO%' } },
          { documentType: { [Op.iLike]: '%PURCHASE_ORDER%' } },
          { documentType: { [Op.iLike]: '%PO%' } }
        ]
      });
    } else {
      andParts.push({ documentType: { [Op.iLike]: `%${type}%` } });
    }
  }

  if (status && status !== 'ALL') {
    if (status === 'SUCCESSFUL') {
      andParts.push({
        validationStatus: { [Op.in]: ['SUCCESSFUL', 'MATCH', 'APPROVED'] }
      });
    } else if (status === 'UNSUCCESSFUL') {
      // Document-level "failed" outcomes. Per-field columns can still show green for fields that passed.
      // NEED_MANUAL = missing required extraction; not MATCH/SUCCESSFUL/APPROVED.
      andParts.push({
        validationStatus: {
          [Op.in]: ['UNSUCCESSFUL', 'MISMATCH', 'REJECTED', 'NEED_MANUAL']
        }
      });
    } else {
      andParts.push({ validationStatus: status });
    }
  }

  const q = String(search ?? '').trim();
  if (q) {
    const pattern = `%${q}%`;
    const orClause: object[] = [
      { bookingId: { [Op.iLike]: pattern } },
      { claimId: { [Op.iLike]: pattern } },
      { documentType: { [Op.iLike]: pattern } }
    ];
    if (searchIncludeId) {
      // Postgres: `uuid ILIKE '…'` is invalid — cast so id substring search works and does not break the whole OR.
      orClause.unshift(sqlWhere(cast(col('id'), 'TEXT'), { [Op.iLike]: pattern }));
    }
    andParts.push({ [Op.or]: orClause });
  }
}

export function cpcWhereFromAndParts(andParts: Record<string, unknown>[]): Record<string, unknown> {
  if (andParts.length === 0) return {};
  return { [Op.and]: andParts };
}

export function digitsOnly(str: string | null | undefined): string {
    return String(str || "").replace(/\D/g, "");
}

export function normalizeMoney(str: string | null | undefined): string {
    const cleaned = String(str || "").replace(/[^\d.]/g, "");
    const num = cleaned ? Number(cleaned) : NaN;
    if (Number.isNaN(num)) return "";
    return String(Math.round(num));
}

/** Compact key for rule lookup / money detection (spaces, hyphens, underscores removed). */
export function compactFieldKey(rawKey: string): string {
    return String(rawKey || '')
        .trim()
        .toLowerCase()
        .replace(/[\s_-]+/g, '');
}

/**
 * True for MSD/extraction keys that represent rupee amounts (commas / Indian grouping should be ignored).
 */
export function isMoneyFieldKey(rawKey: string): boolean {
    const k = compactFieldKey(rawKey);
    if (!k) return false;
    if (k.includes('amount')) return true;
    if (k.includes('invoicevalue')) return true;
    if (k.includes('totalvalue')) return true;
    if (k.includes('taxamount')) return true;
    return false;
}

/**
 * Lowercase + spaces/hyphens → underscores for all keys; compact camelCase aliases **only for money keys**
 * (e.g. poAmount / Po Amount → po_amount). Non-money keys are unchanged except whitespace normalization.
 */
export function canonicalizeRuleFieldKey(rawKey: string): string {
    const k = String(rawKey || '')
        .trim()
        .toLowerCase()
        .replace(/[\s-]+/g, '_');
    if (!isMoneyFieldKey(k) && !isMoneyFieldKey(rawKey)) {
        return k;
    }
    const compact = k.replace(/_/g, '');
    const amountAliases: Record<string, string> = {
        poamount: 'po_amount',
        letteramount: 'letter_amount',
        invoicevalue: 'invoice_value',
        taxamount: 'tax_amount',
        totalamount: 'total_amount'
    };
    if (amountAliases[compact]) return amountAliases[compact];
    return k;
}

/** Rename payload keys so money fields use canonical snake_case (e.g. poAmount → po_amount). Non-money keys untouched. */
export function canonicalizeMoneyFieldKeysInRecord(obj: Record<string, unknown> | null | undefined): Record<string, unknown> {
    if (!obj || typeof obj !== 'object' || Array.isArray(obj)) return (obj || {}) as Record<string, unknown>;
    const out = { ...obj };
    for (const key of [...Object.keys(out)]) {
        if (!isMoneyFieldKey(key)) continue;
        const nk = canonicalizeRuleFieldKey(key);
        if (nk === key) continue;
        const v = out[key];
        delete out[key];
        if (out[nk] === undefined) out[nk] = v;
    }
    return out;
}

/** Normalize money-type values to plain digit strings (no commas) for MSD / extracted payloads. */
export function sanitizeMoneyValuesInRecord(obj: Record<string, unknown> | null | undefined): Record<string, unknown> {
    if (!obj || typeof obj !== 'object' || Array.isArray(obj)) return (obj || {}) as Record<string, unknown>;
    const out: Record<string, unknown> = { ...obj };
    for (const key of Object.keys(out)) {
        if (!isMoneyFieldKey(key)) continue;
        const v = out[key];
        if (v === null || v === undefined) continue;
        const s = String(v).trim();
        if (!s) continue;
        const nm = normalizeMoney(s);
        if (nm !== '') out[key] = nm;
    }
    return out;
}

/**
 * Strip trailing relation / father-name suffix (S/O, W/O, …) so "Arjun Mehar S/O Radheshyam Mehar" → "Arjun Mehar".
 */
export function trimPatronymicSuffixFromName(s: string | null | undefined): string {
    let t = cleanText(s);
    if (!t) return '';
    const re = /\b(?:s\/o|w\/o|d\/o|c\/o|son\s+of|daughter\s+of|wife\s+of|husband\s+of|care\s+of)\b/i;
    const parts = t.split(re);
    t = (parts[0] ?? t).trim();
    t = t.split(/[,;]/)[0]?.trim() ?? t;
    return cleanText(t);
}

/** Multi-word military / rank prefixes at the start of a name line (longest first). */
const MULTI_TITLE_PREFIX_RES: RegExp[] = [
    /^air\s+vice\s+marshal\s+/i,
    /^air\s+commodore\s+/i,
    /^vice\s+admiral\s+/i,
    /^rear\s+admiral\s+/i,
    /^group\s+captain\s+/i,
    /^wing\s+commander\s+/i,
    /^sqn\s+ldr\.?\s+/i,
    /^flying\s+officer\s+/i,
    /^fg\s+offr\.?\s+/i
];

/** Single-token salutations / ranks at the start (repeat until none). */
const SINGLE_TITLE_PREFIX_RE =
    /^(?:mr|mrs|ms|miss|dr\.?|doctor|prof\.?|sir|madam|shri|smt\.?|smti\.?|kumari|kum\.?|lt\.?|lieut\.?|lieutenant|leftenant|capt\.?|captain|maj\.?|major|col\.?|colonel|brig\.?|brigadier|gen\.?|general|cmdr|commander|cmde|commodore|adm\.?|admiral|hon\.?|honorable|honourable|retd\.?|svc)\s+/i;

function stripLeadingSalutationsAndTitles(s: string): string {
    let t = cleanText(s);
    for (let guard = 0; guard < 24; guard++) {
        let removed = false;
        for (const re of MULTI_TITLE_PREFIX_RES) {
            if (re.test(t)) {
                t = t.replace(re, '').trim();
                removed = true;
                break;
            }
        }
        if (removed) continue;
        if (SINGLE_TITLE_PREFIX_RE.test(t)) {
            t = t.replace(SINGLE_TITLE_PREFIX_RE, '').trim();
            continue;
        }
        break;
    }
    return t;
}

/**
 * Holder-style person name for extraction / compare: no leading Dr./military rank tokens, no S/O-style suffixes.
 */
export function normalizePersonNameExtract(s: string | null | undefined): string {
    if (s == null || !String(s).trim()) return '';
    let t = stripLeadingSalutationsAndTitles(String(s));
    t = trimPatronymicSuffixFromName(t);
    return cleanText(t);
}

/** Strip salutations / relation clutter from holder name fields on an extracted / payload object. */
export function sanitizePersonNameFieldsInRecord(obj: Record<string, unknown> | null | undefined): Record<string, unknown> {
    if (!obj || typeof obj !== 'object' || Array.isArray(obj)) return (obj || {}) as Record<string, unknown>;
    const out = { ...obj };
    for (const key of Object.keys(out)) {
        if (!isPersonalHolderNameField(key)) continue;
        const v = out[key];
        if (v === null || v === undefined) continue;
        const n = normalizePersonNameExtract(String(v));
        if (n) out[key] = n;
    }
    return out;
}

/** Customer / holder person name fields (not supplier, grantor, or company). */
export function isPersonalHolderNameField(rawKey: string): boolean {
    const k = compactFieldKey(rawKey);
    if (!k) return false;
    if (/(vendor|grantor|supplier|dealer|company|business)/.test(k)) return false;
    return (
        k === 'name' ||
        k === 'customername' ||
        k === 'authorizedpersonname' ||
        k === 'accountholdername'
    );
}

export function cleanText(str: string | null | undefined): string {
    return String(str || "").trim().replace(/\s+/g, " ");
}

export function nameTokens(str: string | null | undefined): string[] {
    return cleanText(str)
        .toLowerCase()
        .replace(/[^a-z\s]/g, " ")
        .split(/\s+/)
        .filter(Boolean);
}

export function normalizeDate(str: string | null | undefined): string | null {
    if (!str) return null;
    // Remove non-alphanumeric chars
    const clean = str.replace(/[^a-zA-Z0-9]/g, "");

    // Attempt to match common formats (DDMMYYYY, DD-MM-YYYY, YYYY-MM-DD)
    // 1. DDMMYYYY (8 digits)
    if (/^\d{8}$/.test(clean)) {
        const day = clean.substring(0, 2);
        const month = clean.substring(2, 4);
        const year = clean.substring(4, 8);
        return `${year}-${month}-${day}`;
    }
    // 2. Already ISO-like YYYYMMDD
    if (/^\d{4}\d{2}\d{2}$/.test(clean) && (clean.startsWith("19") || clean.startsWith("20"))) {
        const year = clean.substring(0, 4);
        const month = clean.substring(4, 6);
        const day = clean.substring(6, 8);
        return `${year}-${month}-${day}`;
    }

    // Try Native Date parsing if it has separators
    try {
        const d = new Date(str);
        if (!isNaN(d.getTime())) {
            return d.toISOString().split('T')[0];
        }
    } catch (e) { }

    return null;
}

export function cleanAddress(str: string | null | undefined): string {
    if (!str) return "";
    // Remove "S/O", "C/O", "D/O", "W/O" and following name until a comma or newline
    return str.replace(/(?:[scdw]\/o[:\s]|care\sof[:\s]|son\sof[:\s]|daughter\sof[:\s]|wife\sof[:\s])[^,\n]*(?:,|\n)?/gi, "").trim();
}

export function calculateMatch(expected: string, found: string, key: string = ""): number {
    if (!expected || !found) return 0;

    const lowerKey = key.toLowerCase();
    let expStr = String(expected).trim().toLowerCase();
    let fndStr = String(found).trim().toLowerCase();

    // 1. Date Normalization Special Handling
    if (lowerKey.includes('dob') || lowerKey.includes('date')) {
        const normExp = normalizeDate(expStr);
        const normFnd = normalizeDate(fndStr);
        if (normExp && normFnd && normExp === normFnd) return 100;

        // Fallback to digits only for dates like "28-06-1990" vs "28061990"
        const dExp = expStr.replace(/\D/g, "");
        const dFnd = fndStr.replace(/\D/g, "");
        if (dExp !== "" && dExp === dFnd) return 100;
    }

    // 2. Address Cleanup
    if (lowerKey.includes('address')) {
        fndStr = cleanAddress(fndStr).toLowerCase();
        expStr = cleanAddress(expStr).toLowerCase();
    }

    // 2a. Personal name (MSD): document may print "Arjun Mehar S/O Radheshyam Mehar" while MSD is "Arjun Mehar".
    // Strip S/O-style suffixes from the document side, then pass if the full MSD phrase appears as a whole phrase.
    if (isPersonalHolderNameField(lowerKey)) {
        const expTrim = trimPatronymicSuffixFromName(expStr).toLowerCase().replace(/\s+/g, ' ').trim();
        const fndTrim = trimPatronymicSuffixFromName(fndStr).toLowerCase().replace(/\s+/g, ' ').trim();
        if (expTrim.length >= 2 && fndTrim.length >= 2) {
            const phraseOk = (hay: string, needle: string) => {
                if (hay === needle) return true;
                if (hay.startsWith(needle)) {
                    if (hay.length === needle.length) return true;
                    const next = hay.charAt(needle.length);
                    return /\s|[,;/]/.test(next);
                }
                const esc = needle.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
                return new RegExp(`(^|\\s)${esc}(\\s|$)`).test(hay);
            };
            if (expTrim.length >= 3 && phraseOk(fndTrim, expTrim)) {
                return 100;
            }
            expStr = expTrim;
            fndStr = fndTrim;
        }
    }

    // 2b. Money: ignore commas, ₹, spaces — compare numeric rupees (aligns browser vs API + Gemini "1,93,533")
    if (isMoneyFieldKey(lowerKey)) {
        const expM = normalizeMoney(expStr);
        const fndM = normalizeMoney(fndStr);
        if (expM && fndM && expM === fndM) return 100;
        const a = expM ? Number(expM) : NaN;
        const b = fndM ? Number(fndM) : NaN;
        if (!Number.isNaN(a) && !Number.isNaN(b)) {
            if (Math.abs(a - b) <= 5) return 100;
            const maxv = Math.max(Math.abs(a), Math.abs(b), 1);
            const pct = Math.round(100 - Math.min(100, (Math.abs(a - b) / maxv) * 100));
            return Math.max(0, pct);
        }
    }

    // 3. Exact match
    if (expStr === fndStr) return 100;

    // 4. String Similarity (Levenshtein/Dice)
    const similarity = stringSimilarity.compareTwoStrings(expStr, fndStr);
    const score = Math.round(similarity * 100);

    // 5. Token-based fallback (Good for names/addresses)
    const tokensA = nameTokens(expStr);
    const tokensB = nameTokens(fndStr);
    if (tokensA.length > 0 && tokensB.length > 0) {
        const setA = new Set(tokensA);
        const setB = new Set(tokensB);
        let intersection = 0;
        for (const t of setA) if (setB.has(t)) intersection++;
        const tokenScore = Math.round((intersection / Math.max(setA.size, setB.size)) * 100);
        return Math.max(score, tokenScore);
    }

    return score > 0 ? score : 0;
}
