/**
 * Seed: ClassProgression + ClassFeatureOption from Foundry pf2e + hand-curated overlays.
 *
 * Sources:
 * - Foundry pf2e class JSONs at `server/prisma/data/foundry-pf2e/packs/classes/*.json`
 *   (cloned manually by dev; see SEED-README.md).
 * - SPELL_SLOT_OVERLAY from `data/spell-slot-overlays.ts` (Pitfall #6 mitigation).
 * - CLASS_FEATURE_OPTIONS from `data/class-feature-options.ts` (D-19 choices).
 *
 * Idempotent — safe to re-run.
 *
 * Compound unique keys (generated by Prisma from Plan 01 schema):
 *   - ClassProgression has `@@unique([className, level])`  -> field name: `className_level`
 *   - ClassFeatureOption has `@@unique([optionsRef, optionKey])` -> field name: `optionsRef_optionKey`
 * If Plan 01's `@@unique([...])` declarations are reordered, Prisma swaps the field names.
 * Verify Plan 01's schema field order before assuming. If the order changes, update the
 * findUnique calls below accordingly.
 */

import 'dotenv/config';
import * as fs from 'fs';
import * as path from 'path';
import { PrismaClient, Prisma } from '../src/generated/prisma/client.js';
import { PrismaPg } from '@prisma/adapter-pg';
import { SPELL_SLOT_OVERLAY, type SpellSlotOverlayEntry } from './data/spell-slot-overlays';
import { CLASS_FEATURE_OPTIONS } from './data/class-feature-options';

const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL });
const prisma = new PrismaClient({ adapter });

// Foundry pf2e v8 nests packs under a system subdirectory: `packs/pf2e/classes/`.
// (RESEARCH §Pitfall 5: Foundry pf2e data shape may shift across major versions.
// In v7 this was `packs/classes/`. SEED-README pins us to pf2e-8.0.3.)
const FOUNDRY_CLASSES_DIR = path.join(
  __dirname,
  'data',
  'foundry-pf2e',
  'packs',
  'pf2e',
  'classes',
);

// The 16 D-16 classes. Filename in Foundry = lowercase classname.
const D16_CLASS_NAMES = [
  'Alchemist',
  'Barbarian',
  'Bard',
  'Champion',
  'Cleric',
  'Druid',
  'Fighter',
  'Investigator',
  'Monk',
  'Oracle',
  'Ranger',
  'Rogue',
  'Sorcerer',
  'Swashbuckler',
  'Witch',
  'Wizard',
] as const;

// Map of (className → choiceType, choiceOptionsRef) for the L1 doctrine/school/etc. step.
const L1_CHOICE_MAP: Record<string, { choiceType: string; choiceOptionsRef: string } | null> = {
  Cleric: { choiceType: 'doctrine', choiceOptionsRef: 'cleric-doctrine' },
  Wizard: { choiceType: 'school', choiceOptionsRef: 'wizard-school' },
  Champion: { choiceType: 'cause', choiceOptionsRef: 'champion-cause' },
  Druid: { choiceType: 'order', choiceOptionsRef: 'druid-order' },
  Sorcerer: { choiceType: 'bloodline', choiceOptionsRef: 'sorcerer-bloodline' },
  Bard: { choiceType: 'muse', choiceOptionsRef: 'bard-muse' },
  Barbarian: { choiceType: 'instinct', choiceOptionsRef: 'barbarian-instinct' },
  Witch: { choiceType: 'patron', choiceOptionsRef: 'witch-patron' },
  Oracle: { choiceType: 'mystery', choiceOptionsRef: 'oracle-mystery' },
  Investigator: { choiceType: 'methodology', choiceOptionsRef: 'investigator-methodology' },
  Ranger: { choiceType: 'edge', choiceOptionsRef: 'ranger-edge' },
  Rogue: { choiceType: 'racket', choiceOptionsRef: 'rogue-racket' },
  Swashbuckler: { choiceType: 'style', choiceOptionsRef: 'swashbuckler-style' },
  Alchemist: { choiceType: 'research-field', choiceOptionsRef: 'alchemist-research-field' },
  Fighter: null, // Fighter has no L1 choice; weapon mastery is at L5
  Monk: null, // Monk's L1 stance is via class feats, not a choiceType
};

// Optional: Fighter L5 weapon-mastery group choice
const HIGHER_LEVEL_CHOICES: Array<{
  className: string;
  level: number;
  choiceType: string;
  choiceOptionsRef: string;
}> = [
  {
    className: 'Fighter',
    level: 5,
    choiceType: 'weapon-mastery-group',
    choiceOptionsRef: 'fighter-weapon-mastery',
  },
];

interface FoundryClassItem {
  level: number;
  name: string;
  uuid?: string;
  img?: string;
}

interface FoundryClassJson {
  name: string;
  type: 'class';
  system: {
    hp: number;
    keyAbility: string[];
    spellcasting: number;
    savingThrows: { fortitude: number; reflex: number; will: number };
    attacks: Record<string, number | { rank: number; name?: string }>;
    defenses: Record<string, number>;
    classFeatLevels?: { value: number[] };
    items: Record<string, FoundryClassItem>;
  };
}

function readFoundryClass(className: string): FoundryClassJson {
  // Foundry filename convention: lowercase, e.g. 'fighter.json'
  const filename = `${className.toLowerCase()}.json`;
  const fullPath = path.join(FOUNDRY_CLASSES_DIR, filename);
  if (!fs.existsSync(fullPath)) {
    throw new Error(
      `Foundry pf2e clone not found at ${fullPath}. ` +
        `See .planning/phases/01-level-up-pf2e-regelkonform/SEED-README.md for setup.`,
    );
  }
  const raw = fs.readFileSync(fullPath, 'utf-8');
  const json = JSON.parse(raw) as FoundryClassJson;
  if (json.type !== 'class' || !json.name || !json.system) {
    throw new Error(`Class JSON does not match expected schema: ${fullPath}`);
  }
  return json;
}

/** Map Foundry numeric proficiency rank (0..8) to our Proficiency string enum. */
function profFromValue(
  v: number | undefined,
): 'UNTRAINED' | 'TRAINED' | 'EXPERT' | 'MASTER' | 'LEGENDARY' {
  if (v === undefined || v === null) return 'UNTRAINED';
  if (v >= 8) return 'LEGENDARY';
  if (v >= 6) return 'MASTER';
  if (v >= 4) return 'EXPERT';
  if (v >= 2) return 'TRAINED';
  return 'UNTRAINED';
}

/**
 * Aggregate all Foundry items at this level and the spell-slot overlay entries at this level
 * into a single ClassProgression upsert payload.
 *
 * Returns a Prisma-compatible create/update input. Prisma `Json?` fields receive
 * `Prisma.JsonNull` (the typed-null sentinel) when the row has no value at that level
 * — passing JS `null` would not compile against `InputJsonValue | NullableJsonNullValueInput`.
 */
function buildProgressionRow(
  className: string,
  level: number,
  foundry: FoundryClassJson,
): Prisma.ClassProgressionUncheckedCreateInput {
  // 1. Grants from Foundry items
  const grants: string[] = Object.values(foundry.system.items)
    .filter(item => item.level === level)
    .map(item => item.name);

  // 2. Proficiency changes — for L1 only, take the class's base proficiency from the JSON.
  let proficiencyChanges: Record<string, string> = {};
  if (level === 1) {
    proficiencyChanges = {
      fortitude: profFromValue(foundry.system.savingThrows.fortitude),
      reflex: profFromValue(foundry.system.savingThrows.reflex),
      will: profFromValue(foundry.system.savingThrows.will),
    };
  }

  // 3. Spell-slot overlay merge
  const overlayEntries: SpellSlotOverlayEntry[] = (SPELL_SLOT_OVERLAY[className] || []).filter(
    e => e.level === level,
  );
  const slotEntry = overlayEntries.find(e => e.spellSlotIncrement);
  const cantripEntry = overlayEntries.find(e => e.cantripIncrement !== undefined);
  const repertoireEntry = overlayEntries.find(e => e.repertoireIncrement !== undefined);

  // 4. Choice type for this (class, level)
  let choiceType: string | null = null;
  let choiceOptionsRef: string | null = null;
  if (level === 1 && L1_CHOICE_MAP[className]) {
    const choice = L1_CHOICE_MAP[className]!;
    choiceType = choice.choiceType;
    choiceOptionsRef = choice.choiceOptionsRef;
  }
  const higherChoice = HIGHER_LEVEL_CHOICES.find(
    c => c.className === className && c.level === level,
  );
  if (higherChoice) {
    choiceType = higherChoice.choiceType;
    choiceOptionsRef = higherChoice.choiceOptionsRef;
  }

  return {
    className,
    level,
    grants,
    proficiencyChanges,
    spellSlotIncrement: slotEntry?.spellSlotIncrement ?? Prisma.JsonNull,
    cantripIncrement: cantripEntry?.cantripIncrement ?? null,
    repertoireIncrement: repertoireEntry?.repertoireIncrement ?? null,
    choiceType,
    choiceOptionsRef,
  };
}

async function seedClassProgression(): Promise<{
  created: number;
  updated: number;
  errors: number;
}> {
  let created = 0;
  let updated = 0;
  let errors = 0;
  console.log('Seeding ClassProgression for 16 classes x 20 levels...');

  for (const className of D16_CLASS_NAMES) {
    let foundry: FoundryClassJson;
    try {
      foundry = readFoundryClass(className);
    } catch (e) {
      errors++;
      console.error(`Failed to load Foundry class ${className}:`, (e as Error).message);
      continue;
    }
    for (let level = 1; level <= 20; level++) {
      const row = buildProgressionRow(className, level, foundry);
      try {
        // Compound unique key field name `className_level` is generated from
        // `@@unique([className, level])` declared in Plan 01's schema. If that
        // declaration is reordered, Prisma will name the key `level_className`
        // instead — verify schema before changing.
        const existing = await prisma.classProgression.findUnique({
          where: { className_level: { className, level } },
        });
        if (existing) {
          await prisma.classProgression.update({
            where: { id: existing.id },
            data: row,
          });
          updated++;
        } else {
          await prisma.classProgression.create({ data: row });
          created++;
        }
      } catch (e) {
        errors++;
        console.error(`Failed to seed ${className} L${level}:`, (e as Error).message);
      }
    }
  }
  console.log(`  ClassProgression: ${created} created, ${updated} updated, ${errors} errors`);
  return { created, updated, errors };
}

async function seedClassFeatureOptions(): Promise<{
  created: number;
  updated: number;
  errors: number;
}> {
  let created = 0;
  let updated = 0;
  let errors = 0;
  console.log(`Seeding ${CLASS_FEATURE_OPTIONS.length} ClassFeatureOption rows...`);

  for (const opt of CLASS_FEATURE_OPTIONS) {
    try {
      // Compound unique key field name `optionsRef_optionKey` is generated from
      // `@@unique([optionsRef, optionKey])` in Plan 01's schema. Same caveat as above
      // applies — verify the field order before touching this.
      const existing = await prisma.classFeatureOption.findUnique({
        where: {
          optionsRef_optionKey: { optionsRef: opt.optionsRef, optionKey: opt.optionKey },
        },
      });
      if (existing) {
        await prisma.classFeatureOption.update({
          where: { id: existing.id },
          data: {
            name: opt.name,
            nameGerman: opt.nameGerman ?? null,
            description: opt.description,
            grants: opt.grants,
            proficiencyChanges: opt.proficiencyChanges ?? Prisma.JsonNull,
          },
        });
        updated++;
      } else {
        await prisma.classFeatureOption.create({
          data: {
            optionsRef: opt.optionsRef,
            optionKey: opt.optionKey,
            name: opt.name,
            nameGerman: opt.nameGerman ?? null,
            description: opt.description,
            grants: opt.grants,
            proficiencyChanges: opt.proficiencyChanges ?? Prisma.JsonNull,
          },
        });
        created++;
      }
    } catch (e) {
      errors++;
      console.error(
        `Failed to seed option ${opt.optionsRef}/${opt.optionKey}:`,
        (e as Error).message,
      );
    }
  }
  console.log(`  ClassFeatureOption: ${created} created, ${updated} updated, ${errors} errors`);
  return { created, updated, errors };
}

async function main(): Promise<void> {
  const cp = await seedClassProgression();
  const cfo = await seedClassFeatureOptions();
  const totalErrors = cp.errors + cfo.errors;
  if (totalErrors > 0) {
    console.error(`\nSeed completed with ${totalErrors} errors.`);
    process.exit(1);
  }
  console.log('\nClassProgression + ClassFeatureOption seed complete.');
}

main()
  .catch(e => {
    console.error(e);
    process.exit(1);
  })
  .finally(() => prisma.$disconnect());
