import { simplifyForMatch } from "./fuzzyMatch.js";
import {
  artifactPieces,
  characterAliases,
  genshinGameData,
  globalMainStats,
  globalSubstats,
  knownCharacters,
  knownSets,
  mainStatsBySlot,
  pieceAliases,
  setAliases,
  slotAliases,
  slotNames,
  statAliases,
} from "./genshinData.js";

export type LookupMatchSource = "exact" | "alias" | "fuzzy" | "missing";

export interface LookupMatch {
  value: string;
  confidence: number;
  source: LookupMatchSource;
}

export interface LookupValidationStatus {
  valid: boolean;
  errors: string[];
  warnings: string[];
  summary: {
    artifactSets: number;
    artifactPieces: number;
    characters: number;
    stats: number;
    generatedAt: string;
    sourceVersion: string;
  };
}

type LookupData = typeof genshinGameData & {
  lookup?: {
    normalizedKeys?: {
      sets?: Record<string, string>;
      pieces?: Record<string, string>;
      slots?: Record<string, string>;
      stats?: Record<string, string>;
      characters?: Record<string, string>;
    };
    goodKeys?: {
      sets?: Record<string, string>;
      pieces?: Record<string, string>;
      stats?: Record<string, string>;
      characters?: Record<string, string>;
    };
    setToPieces?: Record<string, string[]>;
    validation?: {
      valid?: boolean;
      errors?: string[];
      warnings?: string[];
    };
  };
};

const lookupData = genshinGameData as LookupData;

const normalizedSets = lookupData.lookup?.normalizedKeys?.sets ?? buildNormalizedMap(knownSets);
const normalizedPieces = lookupData.lookup?.normalizedKeys?.pieces ?? buildNormalizedMap(artifactPieces.map((piece) => piece.name));
const normalizedSlots = lookupData.lookup?.normalizedKeys?.slots ?? buildNormalizedMap(slotNames);
const normalizedStats = lookupData.lookup?.normalizedKeys?.stats ?? buildNormalizedMap([...globalMainStats, ...globalSubstats]);
const normalizedCharacters = lookupData.lookup?.normalizedKeys?.characters ?? buildNormalizedMap(knownCharacters);

export function normalizeLookupKey(value: string) {
  return simplifyForMatch(value).replace(/[^a-z0-9]+/g, "");
}

export function matchPiece(raw: string, minConfidence = 0.72): LookupMatch {
  return matchLookup(raw, normalizedPieces, pieceAliases, minConfidence);
}

export function matchSet(raw: string, minConfidence = 0.72): LookupMatch {
  return matchLookup(raw, normalizedSets, setAliases, minConfidence);
}

export function matchSlot(raw: string, minConfidence = 0.72): LookupMatch {
  return matchLookup(raw, normalizedSlots, slotAliases, minConfidence);
}

export function matchStat(raw: string, minConfidence = 0.72): LookupMatch {
  return matchLookup(raw, normalizedStats, statAliases, minConfidence);
}

export function matchCharacter(raw: string, minConfidence = 0.72): LookupMatch {
  return matchLookup(raw, normalizedCharacters, characterAliases, minConfidence);
}

export function validateLookupPackage(data: LookupData = lookupData): LookupValidationStatus {
  const errors: string[] = [];
  const warnings: string[] = [];
  const pieces = data.artifactPieces ?? artifactPieces;
  const sets = data.artifactSets ?? [];
  const characters = data.characters ?? [];
  const slots = data.slots ?? slotNames;
  const stats = data.stats?.main ?? data.mainStats ?? [];
  const setNames = new Set(sets.map((set) => set.name).filter(Boolean));
  const slotNameSet = new Set(slots);

  if (!sets.length) errors.push("No artifact sets in lookup package.");
  if (!pieces.length) errors.push("No artifact pieces in lookup package.");
  if (!characters.length) warnings.push("No characters in lookup package.");

  for (const piece of pieces) {
    if (!piece.name) errors.push("Artifact piece without name.");
    if (!piece.setName || !setNames.has(piece.setName)) errors.push(`Piece "${piece.name}" references missing set "${piece.setName}".`);
    if (!piece.slot || !slotNameSet.has(piece.slot)) errors.push(`Piece "${piece.name}" references missing slot "${piece.slot}".`);
  }

  const goodSets = Object.values(data.lookup?.goodKeys?.sets ?? {});
  const duplicateGoodSet = firstDuplicate(goodSets);
  if (duplicateGoodSet) errors.push(`Duplicate GOOD set key "${duplicateGoodSet}".`);

  for (const [alias, target] of Object.entries(data.aliases?.stats ?? {})) {
    if (!stats.includes(target) && !(data.stats?.sub ?? data.substats ?? []).includes(target)) {
      errors.push(`Stat alias "${alias}" targets unknown stat "${target}".`);
    }
  }

  const generatedAt = typeof data.generatedAt === "string" ? data.generatedAt : "";
  const sourceVersion = typeof data.sourceVersion === "string" ? data.sourceVersion : "unknown";
  if (!generatedAt) warnings.push("Lookup package has no generatedAt timestamp.");

  return {
    valid: errors.length === 0,
    errors,
    warnings,
    summary: {
      artifactSets: sets.length,
      artifactPieces: pieces.length,
      characters: characters.length,
      stats: stats.length,
      generatedAt,
      sourceVersion,
    },
  };
}

function matchLookup(
  raw: string,
  normalized: Record<string, string>,
  aliases: Record<string, string>,
  minConfidence: number,
): LookupMatch {
  const key = normalizeLookupKey(raw);
  if (!key) return missingMatch();

  const alias = Object.entries(aliases).find(([from]) => normalizeLookupKey(from) === key);
  if (alias) return { value: alias[1], confidence: 96, source: "alias" };

  if (normalized[key]) return { value: normalized[key], confidence: 100, source: "exact" };

  let bestValue = "";
  let bestScore = 0;
  for (const [candidateKey, value] of Object.entries(normalized)) {
    const score = similarity(key, candidateKey);
    if (score > bestScore) {
      bestScore = score;
      bestValue = value;
    }
  }

  return bestScore >= minConfidence
    ? { value: bestValue, confidence: Math.round(bestScore * 100), source: "fuzzy" }
    : missingMatch();
}

function missingMatch(): LookupMatch {
  return { value: "", confidence: 0, source: "missing" };
}

function buildNormalizedMap(values: string[]) {
  return Object.fromEntries(values.filter(Boolean).map((value) => [normalizeLookupKey(value), value]));
}

function firstDuplicate(values: string[]) {
  const seen = new Set<string>();
  for (const value of values) {
    if (seen.has(value)) return value;
    seen.add(value);
  }
  return "";
}

function similarity(a: string, b: string) {
  if (a === b) return 1;
  if (!a || !b) return 0;
  const distance = levenshtein(a, b);
  return 1 - distance / Math.max(a.length, b.length);
}

function levenshtein(a: string, b: string) {
  const previous = Array.from({ length: b.length + 1 }, (_, index) => index);
  const current = new Array<number>(b.length + 1);
  for (let i = 1; i <= a.length; i++) {
    current[0] = i;
    for (let j = 1; j <= b.length; j++) {
      const cost = a[i - 1] === b[j - 1] ? 0 : 1;
      current[j] = Math.min(current[j - 1] + 1, previous[j] + 1, previous[j - 1] + cost);
    }
    previous.splice(0, previous.length, ...current);
  }
  return previous[b.length];
}
