import type {
  Artifact,
  ArtifactSlot,
  BuildSuggestion,
  Character,
  CharacterPreset,
  Recommendation,
} from "../types/domain";

const freeSlots: ArtifactSlot[] = ["flower", "plume"];
const slots: ArtifactSlot[] = ["flower", "plume", "sands", "goblet", "circlet"];

function statValueScore(stat: string, value: number) {
  if (stat.includes("CRIT")) return value / 7;
  if (stat === "Energy Recharge") return value / 6;
  if (stat === "Elemental Mastery") return value / 30;
  if (stat.endsWith("%")) return value / 7;
  return value / 50;
}

export function scoreArtifactForPreset(artifact: Artifact, preset: CharacterPreset) {
  const allowedMainStats = preset.mainStats[artifact.slot];
  const mainStatFits =
    freeSlots.includes(artifact.slot) ||
    !allowedMainStats ||
    allowedMainStats.includes(artifact.mainStat);

  if (!mainStatFits) return 0;

  const setBonus =
    preset.recommendedSets.includes(artifact.setKey) ? 22 :
    preset.alternativeSets.includes(artifact.setKey) ? 12 :
    0;

  const mainStatBonus = freeSlots.includes(artifact.slot) ? 8 : 24;
  const substatScore = artifact.substats.reduce((total, substat) => {
    const weight = preset.substatWeights[substat.key] ?? 0;
    return total + weight * statValueScore(substat.key, substat.value);
  }, 0);

  return Math.round((setBonus + mainStatBonus + substatScore + artifact.level * 0.35) * 10) / 10;
}

export function recommendArtifacts(
  artifacts: Artifact[],
  characters: Character[],
  presets: CharacterPreset[],
): Recommendation[] {
  const ownedPresets = presets.filter((preset) =>
    characters.some((character) => character.id === preset.characterId && character.owned),
  );

  return artifacts.map((artifact) => {
    if (artifact.confidence < 0.82) {
      return {
        artifactId: artifact.id,
        verdict: "needs_review",
        score: 0,
        bestCharacters: [],
        reason: "Scanner confidence is low. Review this piece before trusting any recommendation.",
      };
    }

    const ranked = ownedPresets
      .map((preset) => ({
        preset,
        score: scoreArtifactForPreset(artifact, preset),
      }))
      .sort((a, b) => b.score - a.score);

    const best = ranked[0];
    const bestCharacters = ranked
      .filter((entry) => entry.score >= Math.max(22, best?.score * 0.78))
      .slice(0, 3)
      .map((entry) => entry.preset.characterId);

    if (!best || best.score < 12) {
      return {
        artifactId: artifact.id,
        verdict: "trash_candidate",
        score: best?.score ?? 0,
        bestCharacters: [],
        reason: "No owned character preset strongly wants this main stat, set, or substat mix.",
      };
    }

    const verdict =
      best.score >= 48 ? "keep" :
      best.score >= 34 ? "character_specific" :
      best.score >= 24 ? "maybe_level" :
      "trash_candidate";

    return {
      artifactId: artifact.id,
      verdict,
      score: best.score,
      bestCharacters,
      reason: `Best fit is ${best.preset.characterId.replaceAll("_", " ")}: ${best.preset.explanation}`,
    };
  });
}

export function suggestBuilds(
  artifacts: Artifact[],
  characters: Character[],
  presets: CharacterPreset[],
): BuildSuggestion[] {
  const suggestions: BuildSuggestion[] = [];

  for (const character of characters.filter((entry) => entry.owned)) {
    const preset = presets.find((entry) => entry.characterId === character.id);
    if (!preset) continue;

    const bySlot = new Map<ArtifactSlot, Artifact[]>();
    for (const slot of slots) {
      bySlot.set(
        slot,
        artifacts
          .filter((artifact) => artifact.slot === slot && artifact.confidence >= 0.82)
          .map((artifact) => ({
            artifact,
            score: scoreArtifactForPreset(artifact, preset),
          }))
          .filter((entry) => entry.score > 0)
          .sort((a, b) => b.score - a.score)
          .slice(0, 8)
          .map((entry) => entry.artifact),
      );
    }

    const candidates = ["recommended_set", "alternative_set", "rainbow"] as const;
    let addedForCharacter = 0;
    const usedBuilds = new Set<string>();
    for (const quality of candidates) {
      const build = pickBuild(quality, preset, bySlot);
      if (!build) continue;
      const buildKey = build.map((artifact) => artifact.id).sort().join("|");
      if (usedBuilds.has(buildKey)) continue;
      usedBuilds.add(buildKey);

      const score = build.reduce((total, artifact) => total + scoreArtifactForPreset(artifact, preset), 0);
      const warnings = buildWarnings(build, preset, character);
      suggestions.push({
        id: `${character.id}-${quality}`,
        characterId: character.id,
        label:
          quality === "recommended_set" ? "Best recommended set" :
          quality === "alternative_set" ? "Best fallback set" :
          "Best stat-stick build",
        quality,
        artifactIds: build.map((artifact) => artifact.id),
        score: Math.round(score * 10) / 10,
        warnings,
        explanation:
          quality === "recommended_set"
            ? "Uses the preferred 4-piece path with the best available off-piece."
            : quality === "alternative_set"
              ? describeFallbackBuild(build, preset)
              : "Ignores set perfection and takes the strongest available stats.",
      });
      addedForCharacter++;
    }

    if (addedForCharacter === 0) {
      const partial = pickPartialBuild(bySlot);
      if (partial.length > 0) {
        const score = partial.reduce((total, artifact) => total + scoreArtifactForPreset(artifact, preset), 0);
        const missingSlots = slots.filter((slot) => !partial.some((artifact) => artifact.slot === slot));
        suggestions.push({
          id: `${character.id}-partial`,
          characterId: character.id,
          label: "Best partial build",
          quality: "rainbow",
          artifactIds: partial.map((artifact) => artifact.id),
          score: Math.round(score * 10) / 10,
          warnings: [
            `Missing ${missingSlots.join(", ")} before this becomes a full build.`,
            ...equippedConflictWarnings(partial, character),
          ],
          explanation: "Uses the strongest currently scanned pieces and shows what is still missing.",
        });
      }
    }
  }

  return suggestions.sort((a, b) => b.score - a.score);
}

function pickBuild(
  quality: BuildSuggestion["quality"],
  preset: CharacterPreset,
  bySlot: Map<ArtifactSlot, Artifact[]>,
) {
  const combinations = enumerateBuilds(bySlot);
  if (combinations.length === 0) return null;

  return combinations
    .filter((build) => buildMatchesQuality(build, quality, preset))
    .sort((a, b) => buildScore(b, preset) - buildScore(a, preset))[0] ?? null;
}

function pickPartialBuild(bySlot: Map<ArtifactSlot, Artifact[]>) {
  const build: Artifact[] = [];
  for (const artifacts of bySlot.values()) {
    if (artifacts[0]) build.push(artifacts[0]);
  }
  return build;
}

function enumerateBuilds(bySlot: Map<ArtifactSlot, Artifact[]>) {
  const candidates = slots.map((slot) => bySlot.get(slot)?.slice(0, 6) ?? []);
  if (candidates.some((artifacts) => artifacts.length === 0)) return [];

  const builds: Artifact[][] = [];
  for (const flower of candidates[0]) {
    for (const plume of candidates[1]) {
      for (const sands of candidates[2]) {
        for (const goblet of candidates[3]) {
          for (const circlet of candidates[4]) {
            builds.push([flower, plume, sands, goblet, circlet]);
          }
        }
      }
    }
  }
  return builds;
}

function buildMatchesQuality(build: Artifact[], quality: BuildSuggestion["quality"], preset: CharacterPreset) {
  if (quality === "rainbow") return true;
  const setCounts = countSets(build);

  if (quality === "recommended_set") {
    return preset.recommendedSets.some((setKey) => (setCounts[setKey] ?? 0) >= 4);
  }

  if (preset.alternativeSets.some((setKey) => (setCounts[setKey] ?? 0) >= 4)) return true;
  const twoPieceSets = Object.entries(setCounts)
    .filter(([setKey, count]) => count >= 2 && [...preset.recommendedSets, ...preset.alternativeSets].includes(setKey))
    .map(([setKey]) => setKey);
  return twoPieceSets.length >= 2;
}

function buildScore(build: Artifact[], preset: CharacterPreset) {
  return build.reduce((total, artifact) => total + scoreArtifactForPreset(artifact, preset), 0) + setShapeBonus(build, preset);
}

function setShapeBonus(build: Artifact[], preset: CharacterPreset) {
  const setCounts = countSets(build);
  if (preset.recommendedSets.some((setKey) => (setCounts[setKey] ?? 0) >= 4)) return 30;
  if (preset.alternativeSets.some((setKey) => (setCounts[setKey] ?? 0) >= 4)) return 22;
  const twoPieceSets = Object.entries(setCounts).filter(([, count]) => count >= 2).length;
  return twoPieceSets >= 2 ? 14 : 0;
}

function countSets(build: Artifact[]) {
  return build.reduce<Record<string, number>>((counts, artifact) => {
    counts[artifact.setKey] = (counts[artifact.setKey] ?? 0) + 1;
    return counts;
  }, {});
}

function describeFallbackBuild(build: Artifact[], preset: CharacterPreset) {
  const setCounts = countSets(build);
  const alternative4p = preset.alternativeSets.find((setKey) => (setCounts[setKey] ?? 0) >= 4);
  if (alternative4p) return "Uses the best available alternative 4-piece path because the ideal set is incomplete.";

  const twoPieceSets = Object.entries(setCounts)
    .filter(([, count]) => count >= 2)
    .map(([setKey]) => setKey.replaceAll("_", " "))
    .slice(0, 2);
  if (twoPieceSets.length >= 2) return `Uses a practical 2pc+2pc fallback: ${twoPieceSets.join(" + ")}.`;

  return "Uses the next best set path because the ideal pieces are incomplete.";
}

function buildWarnings(build: Artifact[], preset: CharacterPreset, character: Character) {
  const warnings: string[] = [];
  const setCounts = countSets(build);

  if (!preset.recommendedSets.some((setKey) => setCounts[setKey] >= 4)) {
    warnings.push("No full preferred 4-piece set yet.");
  }

  const hasEr = build.some((artifact) =>
    artifact.mainStat === "Energy Recharge" ||
    artifact.substats.some((substat) => substat.key === "Energy Recharge"),
  );
  if (preset.erTarget && !hasEr) warnings.push(`ER target around ${preset.erTarget}% may be hard to reach.`);
  warnings.push(...equippedConflictWarnings(build, character));

  return warnings;
}

function equippedConflictWarnings(build: Artifact[], character: Character) {
  const conflicts = build
    .filter((artifact) => artifact.equipped && simplify(artifact.equipped) !== simplify(character.name))
    .map((artifact) => `${artifact.slot} from ${artifact.equipped}`);
  return conflicts.length ? [`Conflicts: ${conflicts.slice(0, 3).join(", ")}${conflicts.length > 3 ? "..." : ""}.`] : [];
}

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