/**
 * CDT code lookup — pure keyword search against the fee schedule JSON.
 * No LLM needed: descriptions are already in plain English.
 *
 * Input: free-text procedure names from the chat ("perio exam", "adult cleaning", "D0120")
 * Output: matched CDT code + description
 */
import path from "path";
import { readFileSync } from "fs";

interface ProcedureCode {
  "Procedure Code": string;
  Description: string;
}

interface CdtMatch {
  code: string;
  description: string;
  input: string;
  toothNumber?: string;
  toothSurface?: string;
  quad?: string;
}

// Load once at module init
const DATA_PATH = path.join(__dirname, "../data/procedureCodes.json");
const ALL_CODES: ProcedureCode[] = JSON.parse(readFileSync(DATA_PATH, "utf8"));

// Pre-build token sets for every code so scoring is O(1) per token
const CODE_TOKENS: { code: string; description: string; tokens: Set<string> }[] =
  ALL_CODES.map((row) => ({
    code: row["Procedure Code"],
    description: row.Description,
    tokens: new Set(
      row.Description.toLowerCase()
        .replace(/[^a-z0-9\s]/g, " ")
        .split(/\s+/)
        .filter(Boolean)
    ),
  }));

// Common aliases that don't appear verbatim in descriptions
const ALIAS_MAP: Record<string, string> = {
  // Exams
  "perio exam":            "periodic oral evaluation",
  "periodic exam":         "periodic oral evaluation",
  "recall exam":           "periodic oral evaluation",
  "comp exam":             "comprehensive oral evaluation",
  "comprehensive exam":    "comprehensive oral evaluation",
  "new patient exam":      "comprehensive oral evaluation",
  "new patient":           "comprehensive oral evaluation",
  "limited exam":          "limited oral evaluation",
  "emergency exam":        "limited oral evaluation",
  // Prophylaxis / cleaning
  "adult cleaning":        "prophylaxis adult",
  "adult prophy":          "prophylaxis adult",
  "adult prophylaxis":     "prophylaxis adult",
  "child cleaning":        "prophylaxis child",
  "child prophy":          "prophylaxis child",
  "pedo cleaning":         "prophylaxis child",
  "kid prophy":            "prophylaxis child",
  // X-rays
  "full mouth xray":       "intraoral complete series",
  "fmx":                   "intraoral complete series",
  "pano":                  "panoramic",
  "panorex":               "panoramic",
  "bitewing":              "bitewing",
  "bw":                    "bitewing two",
  "2bw":                   "bitewing two",
  "4bw":                   "bitewing four",
  "periapical":            "periapical first",
  // Fluoride / sealant
  "sealant":               "sealant",
  "fluoride":              "fluoride",
  "fl":                    "fluoride",
  "fl varnish":            "fluoride varnish",
  "fluoride treatment":    "fluoride",
  "fluoride application":  "fluoride",
  "topical fluoride":      "fluoride",
  // Perio
  "scaling root planing":  "scaling root planing",
  "srp":                   "scaling root planing",
  "srp 4":                 "scaling root planing four",
  "srp 1 3":               "scaling root planing one",
  "perio maintenance":     "periodontal maintenance",
  "perio maintains":       "periodontal maintenance",
  "perio maint":           "periodontal maintenance",
  // Fillings / restorations
  "filling":               "resin",
  "composite":             "resin",
  "amalgam":               "amalgam",
  "core buildup":          "core buildup",
  "core bu":               "core buildup",
  "buildup":               "core buildup",
  // Crowns
  "crown":                 "crown",
  "porcelain crown":       "porcelain ceramic",
  "pfm":                   "porcelain fused",
  "pfc":                   "porcelain fused",
  "high noble crown":      "porcelain fused high noble",
  "base metal crown":      "porcelain fused base metal",
  "re cement crown":       "re-cement",
  "recement crown":        "re-cement",
  "recement":              "re-cement",
  "post core":             "post and core",
  "post and core":         "post and core",
  // Root canal (endodontic)
  "root canal":            "endodontic therapy",
  "rct":                   "endodontic therapy",
  "endo":                  "endodontic therapy",
  "anterior rct":          "endodontic anterior",
  "rct anterior":          "endodontic anterior",
  "premolar rct":          "endodontic premolar",
  "rct premolar":          "endodontic premolar",
  "bicuspid rct":          "endodontic premolar",
  "molar rct":             "endodontic molar",
  "rct molar":             "endodontic molar",
  // Extractions
  "extraction":            "extraction",
  "ext":                   "extraction erupted",
  "simple extraction":     "extraction erupted",
  "surgical extraction":   "extraction bone",
  "surgical ext":          "extraction bone",
  "baby tooth ext":        "extraction primary",
  "baby tooth extraction": "extraction primary",
  "primary tooth ext":     "extraction primary",
  "soft tissue impaction":  "impacted soft tissue",
  "partial bony":          "impacted partially bony",
  "partial bony impaction":"impacted partially bony",
  "complete bony":         "impacted completely bony",
  "complete bony impaction":"impacted completely bony",
  "root tip":              "surgical removal residual",
  // Dentures
  "full upper denture":    "complete denture maxillary",
  "complete upper denture":"complete denture maxillary",
  "full lower denture":    "complete denture mandibular",
  "complete lower denture":"complete denture mandibular",
  "partial upper denture": "partial denture maxillary",
  "partial lower denture": "partial denture mandibular",
  // Implant / abutment
  "abutment":              "abutment",
  "implant crown":         "implant crown ceramic",
  // Suture
  "suture":                "suture small wound",
};

// Direct phrase → CDT code for terms not in the MH fee schedule JSON
// or short abbreviations that need precise mapping (checked before keyword search)
const DIRECT_CODE_MAP: Record<string, string> = {
  // X-rays
  "2bw":                    "D0272",
  "2 bw":                   "D0272",
  "4bw":                    "D0274",
  "4 bw":                   "D0274",
  "pa":                     "D0220",
  "1 pa":                   "D0220",
  "first pa":               "D0220",
  "2nd pa":                 "D0230",
  "additional pa":          "D0230",
  "cbct":                   "D0367",
  "cone beam":              "D0367",
  // Fluoride
  "fl":                     "D1208",
  // Perio
  "srp 4+":                 "D4341",
  "srp 1-3":                "D4342",
  // Core / post
  "core bu":                "D2950",
  "p/c":                    "D2954",
  "post":                   "D2954",
  "pos":                    "D2954",
  "post core":              "D2954",
  // Crowns
  "recement":               "D2920",
  "re cement":              "D2920",
  "porcelain crown":        "D2740",
  "pfm":                    "D2750",
  "high noble crown":       "D2750",
  "base metal crown":       "D2751",
  "noble crown":            "D2752",
  // Root canal
  "rct anterior":           "D3310",
  "anterior rct":           "D3310",
  "rct premolar":           "D3320",
  "premolar rct":           "D3320",
  "bicuspid rct":           "D3320",
  "rct molar":              "D3330",
  "molar rct":              "D3330",
  "primary anterior rct":   "D3230",
  "rct primary anterior":   "D3230",
  "primary molar rct":      "D3240",
  "rct primary molar":      "D3240",
  "baby tooth rct":         "D3230",
  // Extractions
  "baby tooth ext":         "D7111",
  "baby tooth extraction":  "D7111",
  "primary tooth ext":      "D7111",
  "surgical extraction":    "D7210",
  "surgical ext":           "D7210",
  "soft tissue impaction":  "D7220",
  "partial bony":           "D7230",
  "complete bony":          "D7240",
  "root tip":               "D7250",
  "suture":                 "D7910",
  // Bone grafts / sinus (not in MH schedule)
  "socket preservation":    "D7953",
  "bone graft socket":      "D7953",
  "socket graft":           "D7953",
  "sinus lift":             "D7951",
  "lateral sinus lift":     "D7951",
  "lateral sinus":          "D7951",
  "vertical sinus lift":    "D7952",
  "vertical sinus":         "D7952",
  "sinus augmentation":     "D7951",
  "bone graft":             "D4263",
  // Implant (not in MH schedule)
  "implant":                "D6010",
  "implant body":           "D6010",
  "custom abutment":        "D6057",
  "abutment":               "D6057",
  "implant crown":          "D6058",
};

/**
 * Extract tooth number and surface from any input containing "#NN" (1-32).
 * e.g. "#29 OB" → { toothNumber: "29", toothSurface: "OB" }
 * e.g. "extraction #14" → { toothNumber: "14" }
 */
function extractToothSurface(input: string): { toothNumber?: string; toothSurface?: string } {
  // Numeric tooth #1-32 (e.g. "#29 OB", "# 32")
  const m = input.match(/#\s*(\d{1,2})(?:\s+([A-Za-z]{1,5}))?/);
  if (m) {
    const toothNum = parseInt(m[1]!, 10);
    if (toothNum >= 1 && toothNum <= 32) {
      const surfaces = (m[2] ?? "").toUpperCase();
      return {
        toothNumber: String(toothNum),
        ...(surfaces && /^[OMDBLFIV]+$/.test(surfaces) ? { toothSurface: surfaces } : {}),
      };
    }
  }
  // Primary/baby tooth letter A-T (e.g. "# H", "#H")
  const pm = input.match(/#\s*([A-Ta-t])\b/);
  if (pm) return { toothNumber: pm[1]!.toUpperCase() };
  return {};
}

// Composite filling tooth classification
const FRONT_TEETH = new Set([6, 7, 8, 9, 10, 11, 22, 23, 24, 25, 26, 27]);
const BACK_TEETH  = new Set([1, 2, 3, 4, 5, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 28, 29, 30, 31, 32]);

// RCT tooth classification by tooth type
const RCT_ANTERIOR  = new Set([6, 7, 8, 9, 10, 11, 22, 23, 24, 25, 26, 27]);   // D3310
const RCT_PREMOLAR  = new Set([4, 5, 12, 13, 20, 21, 28, 29]);                  // D3320
const RCT_MOLAR     = new Set([1, 2, 3, 14, 15, 16, 17, 18, 19, 30, 31, 32]);  // D3330

/**
 * Parse composite filling notation like "#29 OB", "composite #8 MO", "tooth 11 MOD".
 * Returns the correct D233x/D239x code based on tooth location and surface count.
 */
function parseCompositeCode(input: string): CdtMatch | null {
  const s = input.trim();
  // Match #NN SURFACES or tooth NN SURFACES (surfaces = 1-5 letters, valid dental surface chars)
  const m = s.match(/#(\d{1,2})\s+([A-Za-z]{1,5})\b/) ?? s.match(/\btooth\s+(\d{1,2})\s+([A-Za-z]{1,5})\b/i);
  if (!m) return null;

  const toothNum = parseInt(m[1]!, 10);
  const surfaces = m[2]!;

  if (toothNum < 1 || toothNum > 32) return null;
  // Surfaces must only be dental surface letters (O M D B L F I V)
  if (!/^[OMDBLFIV]+$/i.test(surfaces)) return null;

  const surfaceCount = surfaces.length;
  const isFront = FRONT_TEETH.has(toothNum);

  if (!isFront && !BACK_TEETH.has(toothNum)) return null;

  let code: string;
  if (isFront) {
    code = surfaceCount === 1 ? "D2330" : surfaceCount === 2 ? "D2331" : surfaceCount === 3 ? "D2332" : "D2335";
  } else {
    code = surfaceCount === 1 ? "D2391" : surfaceCount === 2 ? "D2392" : surfaceCount === 3 ? "D2393" : "D2394";
  }

  const row = ALL_CODES.find((r) => r["Procedure Code"] === code);
  return { code, description: row?.Description ?? code, input, toothNumber: String(toothNum), toothSurface: surfaces.toUpperCase() };
}

/**
 * Parse SRP code with quadrant: "D4341 UL", "4341 LR", "D4342 UR", etc.
 * UL/UR/LL/LR only appear with D4341 or D4342.
 */
function parseSrpCode(input: string): CdtMatch | null {
  const m = input.trim().match(/^(D?4341|D?4342)\s+(UL|UR|LL|LR)$/i);
  if (!m) return null;
  const base = m[1]!.toUpperCase();
  const codeStr = base.startsWith("D") ? base : "D" + base;
  const quadrant = m[2]!.toUpperCase();
  const row = ALL_CODES.find((r) => r["Procedure Code"].toUpperCase() === codeStr);
  if (!row) return null;
  return { code: codeStr, description: row.Description, input, quad: quadrant };
}

/**
 * Parse RCT/root-canal notation like "rct #29", "#14 rct", "root canal #6".
 * Returns D3310 (anterior), D3320 (premolar), or D3330 (molar) based on tooth number.
 */
function parseRctCode(input: string): CdtMatch | null {
  const lower = input.toLowerCase();
  const isRct = /\b(rct|root\s*canal)\b/.test(lower);
  if (!isRct) return null;

  // Extract tooth number from "#NN" or bare number adjacent to "rct"/"root canal"
  let toothNum: number | null = null;
  const hashMatch = input.match(/#(\d{1,2})/);
  if (hashMatch) {
    toothNum = parseInt(hashMatch[1]!, 10);
  } else {
    // Fallback: bare number in input (e.g. "rct 29", "29 rct")
    const numMatch = input.match(/\b(\d{1,2})\b/);
    if (numMatch) toothNum = parseInt(numMatch[1]!, 10);
  }

  if (!toothNum || toothNum < 1 || toothNum > 32) return null;

  let code: string;
  if (RCT_ANTERIOR.has(toothNum)) {
    code = "D3310";
  } else if (RCT_PREMOLAR.has(toothNum)) {
    code = "D3320";
  } else if (RCT_MOLAR.has(toothNum)) {
    code = "D3330";
  } else {
    return null;
  }

  const row = ALL_CODES.find((r) => r["Procedure Code"].toUpperCase() === code);
  return { code, description: row?.Description ?? code, input, toothNumber: String(toothNum) };
}

/**
 * Score how well a set of query tokens matches a code's description tokens.
 * Each matched token contributes 1 point; shorter descriptions get a bonus
 * so "prophylaxis - adult" beats "prophylaxis - child, up to 14 years of age".
 */
function score(queryTokens: string[], entry: { tokens: Set<string>; description: string }): number {
  let hits = 0;
  for (const t of queryTokens) {
    if (entry.tokens.has(t)) hits++;
  }
  if (hits === 0) return 0;
  // Tie-break: prefer shorter descriptions (more specific match)
  return hits - entry.description.length * 0.0001;
}

/**
 * Map a single free-text procedure name to the best CDT code.
 * Returns null if no reasonable match is found.
 */
function matchOne(input: string): CdtMatch | null {
  const cleaned = input.trim().toLowerCase();

  // Direct CDT code pass-through (e.g. "D0120", "D7111")
  if (/^d\d{4}$/i.test(cleaned)) {
    const row = ALL_CODES.find(
      (r) => r["Procedure Code"].toLowerCase() === cleaned.toLowerCase()
    );
    const code = row?.["Procedure Code"] ?? cleaned.toUpperCase();
    return { code, description: row?.Description ?? code, input };
  }

  // 4-digit shorthand: auto-prefix "D" (e.g. "0120" → "D0120")
  if (/^\d{4}$/.test(cleaned)) {
    const withD = "D" + cleaned;
    const row = ALL_CODES.find((r) => r["Procedure Code"].toUpperCase() === withD.toUpperCase());
    const code = row?.["Procedure Code"] ?? withD.toUpperCase();
    return { code, description: row?.Description ?? code, input };
  }

  // Apply alias before tokenizing
  const normalized = ALIAS_MAP[cleaned] ?? cleaned;
  const queryTokens = normalized
    .replace(/[^a-z0-9\s]/g, " ")
    .split(/\s+/)
    .filter(Boolean);

  if (queryTokens.length === 0) return null;

  let bestScore = 0;
  let bestEntry: { code: string; description: string } | null = null;

  for (const entry of CODE_TOKENS) {
    const s = score(queryTokens, entry);
    if (s > bestScore) {
      bestScore = s;
      bestEntry = { code: entry.code, description: entry.description };
    }
  }

  // Require at least one full token match
  if (!bestEntry || bestScore < 0.9) return null;
  return { code: bestEntry.code, description: bestEntry.description, input };
}

/**
 * Map an array of free-text procedure names to CDT codes.
 * customAliases (from DB) are checked before the hardcoded ALIAS_MAP.
 * Unmatched names are returned with code = null and a note in description.
 */
export function lookupCdtCodes(
  procedureNames: string[],
  customAliases: { phrase: string; cdtCode: string }[] = []
): (CdtMatch | { code: null; description: string; input: string })[] {
  // Build a lookup map from custom aliases (phrase → cdtCode), lowercased
  const customMap: Record<string, string> = {};
  for (const { phrase, cdtCode } of customAliases) {
    if (phrase && cdtCode) customMap[phrase.toLowerCase().trim()] = cdtCode.toUpperCase().trim();
  }

  return procedureNames.flatMap((name) => {
    const cleaned = name.trim().toLowerCase();

    // If the LLM emitted a bare tooth-number token like "# 32" or "#32" with no procedure,
    // silently skip it — the tooth info was already captured on the preceding CDT code entry.
    if (/^#\s*\d{1,2}$/.test(cleaned)) return [];

    // Extract tooth# from "#NN" notation — applies to any procedure
    const toothInfo = extractToothSurface(name);
    // Strip tooth notation for alias matching:
    // numeric (#29), numeric with space (# 3), primary letter (#H, # H)
    const strippedCleaned = cleaned
      .replace(/,?\s*#\s*\d{1,2}\b/g, "")
      .replace(/,?\s*#\s*[a-t]\b/gi, "")
      .replace(/\s+/g, " ")
      .trim();

    // 1. Custom alias exact match (highest priority)
    const customKey = customMap[cleaned] ? cleaned : customMap[strippedCleaned] ? strippedCleaned : null;
    if (customKey) {
      const code = customMap[customKey]!;
      const row = ALL_CODES.find((r) => r["Procedure Code"].toUpperCase() === code);
      return { code, description: row?.Description ?? code, input: name, ...toothInfo };
    }

    // 2. Composite filling by tooth# and surfaces — also determines CDT code from tooth position
    const compositeMatch = parseCompositeCode(name);
    if (compositeMatch) return compositeMatch; // already carries toothNumber/toothSurface

    // 2b. RCT by tooth# — D3310/D3320/D3330 based on tooth position
    const rctMatch = parseRctCode(name);
    if (rctMatch) return rctMatch;

    // 2c. SRP + quadrant — D4341/D4342 with UL/UR/LL/LR
    const srpMatch = parseSrpCode(name);
    if (srpMatch) return srpMatch;

    // 3. Hardcoded direct code map (short abbreviations / codes not in MH schedule)
    const directKey = DIRECT_CODE_MAP[cleaned] ? cleaned : DIRECT_CODE_MAP[strippedCleaned] ? strippedCleaned : null;
    if (directKey) {
      const code = DIRECT_CODE_MAP[directKey]!;
      const row = ALL_CODES.find((r) => r["Procedure Code"].toUpperCase() === code);
      return { code, description: row?.Description ?? code, input: name, ...toothInfo };
    }

    // 4. Hardcoded alias + keyword search (try stripped name so tooth# doesn't break matching)
    const match = matchOne(strippedCleaned !== cleaned ? strippedCleaned : name);
    if (match) return { ...match, input: name, ...toothInfo };

    return {
      code: null,
      description: `No CDT code found for "${name}"`,
      input: name,
    };
  });
}
