/**
 * Development seed. Idempotent: every row is upserted by a natural key, so it is safe to run
 * repeatedly (`pnpm db:seed`). Creates plans, board templates, the original question packs,
 * a platform admin, two pilot venues (for tenant-isolation testing) and the demo venue.
 *
 * Dev passwords come from SEED_ADMIN_PASSWORD / SEED_HOST_PASSWORD (dev defaults otherwise) and are
 * reset on every run so the printed logins always work. Never run against production.
 */
import path from 'node:path';
import { BOARD_TEMPLATES } from '../src/board-templates';
import { hashPassword } from '../src/auth';
import { createDb, type Db } from '../src/client';
import { formatIssue, validatePacks, type ContentPack } from '../src/content/index';
import type { Prisma, VenueRole, VenueStatus } from '../src/generated/prisma/client';
import {
  addDaysToDate,
  defaultChampionshipPlan,
  localDate,
  parseEventConfig,
  parseVenueSettings,
  zonedDateTimeToEpoch,
  type VenueSettingsInput,
} from '../src/settings';

for (const file of [path.resolve(import.meta.dirname, '../../../.env'), path.resolve(import.meta.dirname, '../.env')]) {
  try {
    process.loadEnvFile(file);
  } catch {
    // optional
  }
}

const DEFAULT_ADMIN_PASSWORD = 'pintrush-admin-dev';
const DEFAULT_HOST_PASSWORD = 'pintrush-host-dev';
const TIMEZONE = 'America/New_York';
const DAY_MS = 86_400_000;

const PLANS = [
  {
    key: 'starter',
    name: 'Starter',
    priceCents: 7_900,
    sortOrder: 1,
    features: ['Always-on games', 'TV display', 'QR join'],
  },
  {
    key: 'pro',
    name: 'Pro',
    priceCents: 14_900,
    sortOrder: 2,
    features: ['Everything in Starter', 'Championships', 'Power-ups', 'Venue leagues', 'Reporting'],
  },
  {
    key: 'network',
    name: 'Network',
    priceCents: 24_900,
    sortOrder: 3,
    features: ['Everything in Pro', 'Multi-venue features (when available)'],
  },
] as const;

const EVERY_DAY = [0, 1, 2, 3, 4, 5, 6];

interface VenueSeed {
  slug: string;
  name: string;
  joinCode: string;
  address: string | null;
  status: VenueStatus;
  settings: VenueSettingsInput;
  room: { slug: string; name: string };
  user?: { email: string; name: string; role: VenueRole };
}

const VENUES: VenueSeed[] = [
  {
    slug: 'harbor-tap',
    name: 'The Harbor Tap',
    joinCode: 'HARBOR',
    address: 'Long Island, NY',
    status: 'PILOT',
    settings: {
      alwaysOn: {
        enabled: true,
        packKey: 'originals-v1',
        schedule: EVERY_DAY.map((day) => ({ day, open: '11:00', close: '02:00' })),
      },
      mergeSolos: false,
    },
    room: { slug: 'main', name: 'Main Bar' },
    user: { email: 'host@harbortap.local', name: 'Harbor Tap Owner', role: 'OWNER' },
  },
  {
    slug: 'ocean-ave',
    name: 'Ocean Ave Pub',
    joinCode: 'OCEAN',
    address: null,
    status: 'PILOT',
    settings: {},
    room: { slug: 'taproom', name: 'Taproom' },
    user: { email: 'host@oceanave.local', name: 'Ocean Ave Owner', role: 'OWNER' },
  },
  {
    slug: 'demo',
    name: 'PintRush Demo Bar',
    joinCode: 'DEMO',
    address: null,
    status: 'ACTIVE',
    settings: {
      alwaysOn: {
        enabled: true,
        packKey: 'originals-v1',
        schedule: EVERY_DAY.map((day) => ({ day, open: '00:00', close: '24:00' })),
      },
      mergeSolos: false,
    },
    room: { slug: 'demo', name: 'Demo Bar' },
  },
];

function json(value: unknown): Prisma.InputJsonValue {
  return JSON.parse(JSON.stringify(value)) as Prisma.InputJsonValue;
}

async function seedPlans(db: Db): Promise<void> {
  for (const plan of PLANS) {
    const data = { name: plan.name, priceCents: plan.priceCents, interval: 'month', features: [...plan.features], sortOrder: plan.sortOrder, isPublic: true };
    await db.plan.upsert({ where: { key: plan.key }, create: { key: plan.key, ...data }, update: data });
  }
}

async function seedBoardTemplates(db: Db): Promise<void> {
  // Free the unique `symmetry` slots first in case an older seed used a different order.
  await db.boardTemplate.deleteMany({ where: { key: { notIn: BOARD_TEMPLATES.map((t) => t.key) } } });
  for (const t of BOARD_TEMPLATES) {
    const data = { name: t.name, symmetry: t.symmetry, permutation: t.permutation };
    await db.boardTemplate.upsert({ where: { key: t.key }, create: { key: t.key, ...data }, update: data });
  }
}

interface PackSummary {
  key: string;
  active: number;
  retired: number;
  note: string;
}

async function packCounts(db: Db, packKey: string): Promise<{ active: number; retired: number }> {
  const [active, retired] = await Promise.all([
    db.question.count({ where: { pack: { key: packKey }, status: 'ACTIVE' } }),
    db.question.count({ where: { pack: { key: packKey }, status: 'RETIRED' } }),
  ]);
  return { active, retired };
}

/** Upsert one pack and its questions in a single transaction, so a failure leaves the pack as it was. */
async function seedPack(db: Db, pack: ContentPack, packHasErrors: boolean): Promise<PackSummary> {
  const packData = {
    name: pack.name,
    description: pack.description,
    kind: pack.kind,
    categories: json(pack.categories),
    status: 'PUBLISHED' as const,
  };
  // Never touch questions of a pack with invalid files (or none yet): retiring based on a
  // half-written or missing file would wrongly pull live questions.
  const syncQuestions = !packHasErrors && pack.questions.length > 0;

  await db.$transaction(
    async (tx) => {
      const row = await tx.questionPack.upsert({ where: { key: pack.key }, create: { key: pack.key, ...packData }, update: packData });
      if (!syncQuestions) return;
      for (const q of pack.questions) {
        const data = {
          packId: row.id,
          category: q.category,
          type: q.type,
          difficulty: q.difficulty,
          prompt: q.prompt,
          options: json(q.options),
          correctOptionId: q.correctOptionId,
          explanation: q.explanation,
          wildcardEligible: q.wildcardEligible,
          status: 'ACTIVE' as const,
        };
        await tx.question.upsert({ where: { contentKey: q.contentKey }, create: { contentKey: q.contentKey, ...data }, update: data });
      }
      await tx.question.updateMany({
        where: { packId: row.id, status: 'ACTIVE', contentKey: { notIn: pack.questions.map((q) => q.contentKey) } },
        data: { status: 'RETIRED' },
      });
    },
    { timeout: 120_000, maxWait: 10_000 },
  );

  const note = packHasErrors
    ? 'questions NOT synced (content errors)'
    : pack.questions.length === 0
      ? 'no question files yet; skipped'
      : pack.missingCategories.length
        ? `missing: ${pack.missingCategories.join(', ')}`
        : 'all categories present';
  return { key: pack.key, ...(await packCounts(db, pack.key)), note };
}

async function seedVenue(db: Db, seed: VenueSeed, hostPasswordHash: string) {
  const settings = json(parseVenueSettings(seed.settings));
  const venueData = { name: seed.name, joinCode: seed.joinCode, timezone: TIMEZONE, address: seed.address, status: seed.status, settings };
  const venue = await db.venue.upsert({ where: { slug: seed.slug }, create: { slug: seed.slug, ...venueData }, update: venueData });
  const room = await db.room.upsert({
    where: { venueId_slug: { venueId: venue.id, slug: seed.room.slug } },
    create: { venueId: venue.id, slug: seed.room.slug, name: seed.room.name },
    update: { name: seed.room.name },
  });
  if (seed.user) {
    const userData = { venueId: venue.id, name: seed.user.name, role: seed.user.role, passwordHash: hostPasswordHash };
    await db.venueUser.upsert({ where: { email: seed.user.email }, create: { email: seed.user.email, ...userData }, update: userData });
  }
  return { venue, room };
}

/** Next Friday 21:00 venue-local that is still in the future (today counts if it's Friday before 21:00). */
function nextFridayEvening(nowMs: number): Date {
  const today = localDate(TIMEZONE, nowMs);
  for (let delta = 0; delta <= 7; delta++) {
    const date = addDaysToDate(today, delta);
    if (new Date(`${date}T12:00:00Z`).getUTCDay() !== 5) continue;
    const at = zonedDateTimeToEpoch(TIMEZONE, date, '21:00');
    if (at > nowMs) return new Date(at);
  }
  throw new Error('No upcoming Friday found');
}

async function seedChampionship(db: Db, venueId: string, roomId: string, createdById: string | null) {
  const name = 'Friday Night Championship';
  const scheduledAt = nextFridayEvening(Date.now());
  const existing = await db.event.findFirst({
    where: { venueId, roomId, kind: 'CHAMPIONSHIP', name, status: 'SCHEDULED' },
    orderBy: { createdAt: 'desc' },
  });
  if (existing) {
    // Keep the seeded show upcoming: move a stale (past) one to the next Friday.
    if (existing.scheduledAt && existing.scheduledAt.getTime() > Date.now()) return existing;
    return db.event.update({ where: { id: existing.id }, data: { scheduledAt, version: { increment: 1 } } });
  }
  return db.event.create({
    data: {
      venueId,
      roomId,
      kind: 'CHAMPIONSHIP',
      status: 'SCHEDULED',
      name,
      scheduledAt,
      config: json(parseEventConfig({ plan: defaultChampionshipPlan(), mergeSolos: false })),
      createdById,
    },
  });
}

async function main(): Promise<void> {
  if (process.env.NODE_ENV === 'production' && process.env.SEED_ALLOW_PRODUCTION !== '1') {
    throw new Error('Refusing to seed dev venues and passwords with NODE_ENV=production (set SEED_ALLOW_PRODUCTION=1 to override).');
  }
  const adminPassword = process.env.SEED_ADMIN_PASSWORD || DEFAULT_ADMIN_PASSWORD;
  const hostPassword = process.env.SEED_HOST_PASSWORD || DEFAULT_HOST_PASSWORD;
  const db = createDb();
  try {
    await seedPlans(db);
    await seedBoardTemplates(db);

    const content = validatePacks();
    const packSummaries: PackSummary[] = [];
    const failedPacks: string[] = [];
    for (const pack of content.packs) {
      const hasErrors = content.errors.some((e) => e.packKey === pack.key);
      try {
        packSummaries.push(await seedPack(db, pack, hasErrors));
      } catch (err) {
        // One pack failing (e.g. a database that can't store its characters) must not block the
        // venues and logins below; the run still exits non-zero.
        const message = err instanceof Error ? (err.message.split('\n').at(-1) ?? err.message) : String(err);
        failedPacks.push(`${pack.key}: ${message}`);
        packSummaries.push({ key: pack.key, ...(await packCounts(db, pack.key)), note: 'FAILED, left unchanged (see below)' });
      }
    }

    await db.platformAdmin.upsert({
      where: { email: 'admin@pintrush.local' },
      create: { email: 'admin@pintrush.local', name: 'PintRush Admin', passwordHash: await hashPassword(adminPassword) },
      update: { passwordHash: await hashPassword(adminPassword) },
    });

    const hostHash = await hashPassword(hostPassword);
    const seeded = new Map<string, Awaited<ReturnType<typeof seedVenue>>>();
    for (const v of VENUES) seeded.set(v.slug, await seedVenue(db, v, hostHash));

    const harbor = seeded.get('harbor-tap');
    if (!harbor) throw new Error('harbor-tap was not seeded');
    const pro = await db.plan.findUniqueOrThrow({ where: { key: 'pro' } });
    const entitlement = await db.entitlement.findFirst({ where: { venueId: harbor.venue.id, planId: pro.id } });
    if (!entitlement) {
      await db.entitlement.create({
        data: { venueId: harbor.venue.id, planId: pro.id, status: 'TRIALING', trialEndsAt: new Date(Date.now() + 30 * DAY_MS) },
      });
    }
    const owner = await db.venueUser.findUnique({ where: { email: 'host@harbortap.local' }, select: { id: true } });
    const championship = await seedChampionship(db, harbor.venue.id, harbor.room.id, owner?.id ?? null);

    // ── Summary ──
    const [plans, templates, venues, rooms, users] = await Promise.all([
      db.plan.count(),
      db.boardTemplate.count(),
      db.venue.count(),
      db.room.count(),
      db.venueUser.count(),
    ]);
    console.log('PintRush seed complete');
    console.log(`  plans ${plans} · board templates ${templates} · venues ${venues} · rooms ${rooms} · venue users ${users}`);
    for (const p of packSummaries) console.log(`  pack ${p.key}: ${p.active} active, ${p.retired} retired (${p.note})`);
    console.log(
      `  championship "${championship.name}" at The Harbor Tap: ${championship.scheduledAt?.toLocaleString('en-US', { timeZone: TIMEZONE, dateStyle: 'full', timeStyle: 'short' })} (${TIMEZONE})`,
    );
    console.log('  join codes: HARBOR (The Harbor Tap), OCEAN (Ocean Ave Pub), DEMO (PintRush Demo Bar)');
    // Echo dev defaults; never echo a custom password from the environment.
    const shown = (envName: string, fallback: string) => {
      const value = process.env[envName];
      return !value || value === fallback ? fallback : `<${envName}>`;
    };
    console.log('  dev logins:');
    console.log(`    /admin  admin@pintrush.local / ${shown('SEED_ADMIN_PASSWORD', DEFAULT_ADMIN_PASSWORD)}`);
    console.log(`    /host   host@harbortap.local / ${shown('SEED_HOST_PASSWORD', DEFAULT_HOST_PASSWORD)}`);
    console.log(`    /host   host@oceanave.local  / ${shown('SEED_HOST_PASSWORD', DEFAULT_HOST_PASSWORD)}`);

    if (content.warnings.length > 0) {
      console.log(`  content warnings (${content.warnings.length}):`);
      for (const w of content.warnings) console.log(`    ! ${formatIssue(w)}`);
    }
    if (failedPacks.length > 0) {
      console.error(`  pack sync failures (${failedPacks.length}):`);
      for (const f of failedPacks) console.error(`    x ${f}`);
      process.exitCode = 1;
    }
    if (content.errors.length > 0) {
      console.error(`  content errors (${content.errors.length}) — affected packs were not synced:`);
      for (const e of content.errors) console.error(`    x ${formatIssue(e)}`);
      process.exitCode = 1;
    }
  } finally {
    await db.$disconnect();
  }
}

main().catch((err: unknown) => {
  console.error('Seed failed:', err);
  process.exitCode = 1;
});
