import { STANDARD_EVENTS, KAOS_EVENTS, type GameEvent } from './events.ts';

export interface PendingEvent {
  event: GameEvent;
  kind: 'event' | 'kaos';
}
import { UPGRADES } from './upgrades.ts';

export interface State {
  tick: number;
  troende: number;
  andakt: number;
  andaktRate: number;   // per second
  inflytande: number;
  inflytandeRate: number; // multiplier on the base network-effect curve
  dread: number;
  dreadRate: number;    // per second
  hemlighet: number;    // 0-100, lose at 0
  kaos: number;         // 0-100, triggers events at 100
  kaosRate: number;     // per second
  ritual: number;       // 0-100, win at 100
  ritualRate: number;   // per second
  ritualUnlocked: boolean;
  areas: { rosengard: boolean; varnhem: boolean; torso: boolean };
  upgrades: Set<string>;
  log: LogEntry[];
  phase: 'intro' | 'playing' | 'won' | 'lost' | 'abandoned';
  lostReason?: string;
  usedEventIds: Set<string>;
  flags: Set<string>;
  nextEventTick: number;
  nextKaosTick: number;
  pendingEvent: PendingEvent | null;
  logSeq: number; // increments on every log push — log.length alone can't detect changes once capped at 40
  lastRealTime: number; // epoch ms, refreshed on save — used to compute offline progress on load
}

export interface LogEntry {
  tick: number;
  text: string;
  kind: 'event' | 'kaos' | 'system' | 'upgrade';
}

// Bump this when State shape changes to invalidate old saves
const SAVE_VERSION = '002';
const SAVE_KEY = `djupet_v${SAVE_VERSION}`;

const EVENT_INTERVAL_TICKS = 180;   // ~6s at 30fps
const KAOS_THRESHOLD = 100;
const HEMLIGHET_DRAIN_PER_TROENDE = 0.002; // per second, per troende above 4
const DREAD_HEMLIGHET_DRAIN = 0.35; // per second, saturating with dread — the deeper it goes, the more it's noticed
const RITUAL_HEMLIGHET_DRAIN = 0.25; // per second at ritual=100 — an active ritual is hard to hide
const INFLYTANDE_BASE_RATE = 0.15; // per second, scales with troende^1.2 — a network effect

export function createState(): State {
  return {
    tick: 0,
    troende: 1,
    andakt: 10,
    andaktRate: 0.3,
    inflytande: 0,
    inflytandeRate: 1,
    dread: 0,
    dreadRate: 0,
    hemlighet: 100,
    kaos: 0,
    kaosRate: 0.05,
    ritual: 0,
    ritualRate: 0,
    ritualUnlocked: false,
    areas: { rosengard: false, varnhem: false, torso: false },
    upgrades: new Set(),
    log: [{ tick: 0, text: 'Mötet började i en fuktig källare i Rosengård. Nio stolar. En av dem är alltid lite för varm.', kind: 'system' }],
    phase: 'intro',
    usedEventIds: new Set(),
    flags: new Set(),
    nextEventTick: EVENT_INTERVAL_TICKS,
    nextKaosTick: EVENT_INTERVAL_TICKS * 3,
    pendingEvent: null,
    logSeq: 0,
    lastRealTime: Date.now(),
  };
}

export function startGame(state: State): State {
  if (state.phase !== 'intro') return state;
  return { ...state, phase: 'playing' };
}

export function abandonGame(state: State): State {
  const s = { ...state, log: [...state.log] };
  s.phase = 'abandoned';
  pushLog(s, { tick: s.tick, text: 'Ni lämnar källaren. Cirkeln bryts. Den Sovande sover vidare, ovetande, ofärdig.', kind: 'system' });
  return s;
}

function pickEvent(pool: GameEvent[], state: State): GameEvent | null {
  const available = pool.filter(e => {
    if (state.usedEventIds.has(e.id)) return false;
    const r = e.requires;
    if (!r) return true;
    if (r.minTroende && state.troende < r.minTroende) return false;
    if (r.minDread && state.dread < r.minDread) return false;
    if (r.minRitual && state.ritual < r.minRitual) return false;
    if (r.unlockedArea && !state.areas[r.unlockedArea]) return false;
    if (r.flag && !state.flags.has(r.flag)) return false;
    if (r.completedEventId && !state.usedEventIds.has(r.completedEventId)) return false;
    return true;
  });
  if (!available.length) return null;
  const total = available.reduce((s, e) => s + e.weight, 0);
  let roll = Math.random() * total;
  for (const e of available) {
    roll -= e.weight;
    if (roll <= 0) return e;
  }
  return available[available.length - 1];
}

function pushLog(state: State, entry: LogEntry): void {
  state.log.unshift(entry);
  state.logSeq += 1;
  if (state.log.length > 40) state.log = state.log.slice(0, 40);
}

function applyEffect(state: State, effect: GameEvent['effect']): void {
  if (effect.andakt) state.andakt = Math.max(0, state.andakt + effect.andakt);
  if (effect.hemlighet) state.hemlighet = Math.max(0, Math.min(100, state.hemlighet + effect.hemlighet));
  if (effect.kaos) state.kaos = Math.max(0, Math.min(100, state.kaos + effect.kaos));
  if (effect.dread) state.dread = Math.max(0, state.dread + effect.dread);
  if (effect.troende) state.troende = Math.max(1, state.troende + effect.troende);
  if (effect.ritual && state.ritualUnlocked) state.ritual = Math.min(100, state.ritual + effect.ritual);
}

const DT = 1 / 30; // 30fps

export function tick(state: State): State {
  if (state.phase !== 'playing') return state;

  const s = { ...state, areas: { ...state.areas }, upgrades: new Set(state.upgrades), log: [...state.log], usedEventIds: new Set(state.usedEventIds), flags: new Set(state.flags) };
  s.tick += 1;

  // A choice event is awaiting the player's answer — freeze new events, keep resources flowing.
  if (s.pendingEvent) {
    s.nextEventTick += 1;
    s.nextKaosTick += 1;
  }

  // Resource accumulation
  s.andakt += s.andaktRate * s.troende * DT;
  s.inflytande += INFLYTANDE_BASE_RATE * s.inflytandeRate * Math.pow(s.troende, 1.2) * DT;
  s.dread += s.dreadRate * DT;
  if (s.ritualUnlocked) s.ritual = Math.min(100, s.ritual + s.ritualRate * DT);

  // Kaos accumulation — faster with more troende and more dread
  const kaosScale = 1 + (s.dread / 200);
  s.kaos = Math.min(100, s.kaos + s.kaosRate * kaosScale * DT);

  // Hemlighet drain — more believers, more dread, and a deepening ritual all draw attention
  const troendeDrain = s.troende > 4 ? HEMLIGHET_DRAIN_PER_TROENDE * (s.troende - 4) : 0;
  const dreadDrain = DREAD_HEMLIGHET_DRAIN * (s.dread / (s.dread + 150));
  const ritualDrain = s.ritualUnlocked ? RITUAL_HEMLIGHET_DRAIN * (s.ritual / 100) : 0;
  s.hemlighet = Math.max(0, s.hemlighet - (troendeDrain + dreadDrain + ritualDrain) * DT);

  // Standard event
  if (!s.pendingEvent && s.tick >= s.nextEventTick) {
    const ev = pickEvent(STANDARD_EVENTS, s);
    if (ev) {
      s.usedEventIds.add(ev.id);
      if (ev.choices?.length) {
        pushLog(s, { tick: s.tick, text: ev.text, kind: 'event' });
        s.pendingEvent = { event: ev, kind: 'event' };
      } else {
        pushLog(s, { tick: s.tick, text: ev.text, kind: 'event' });
        applyEffect(s, ev.effect);
      }
    }
    s.nextEventTick = s.tick + EVENT_INTERVAL_TICKS + Math.floor(Math.random() * 60);
  }

  // Kaos event
  if (!s.pendingEvent && (s.kaos >= KAOS_THRESHOLD || s.tick >= s.nextKaosTick)) {
    const ev = pickEvent(KAOS_EVENTS, s);
    if (ev) {
      // Kaos events are chaos-relief flavor, not narrative progression — let them recur
      // over a long game instead of permanently depleting a pool of only ~24 entries.
      if (ev.choices?.length) {
        pushLog(s, { tick: s.tick, text: `⚡ ${ev.text}`, kind: 'kaos' });
        s.pendingEvent = { event: ev, kind: 'kaos' };
      } else {
        pushLog(s, { tick: s.tick, text: `⚡ ${ev.text}`, kind: 'kaos' });
        applyEffect(s, ev.effect);
      }
    }
    s.kaos = Math.max(0, s.kaos - 80);
    s.nextKaosTick = s.tick + EVENT_INTERVAL_TICKS * 5 + Math.floor(Math.random() * 120);
  }

  // Win / lose checks
  if (s.ritual >= 100) {
    s.phase = 'won';
    pushLog(s, { tick: s.tick, text: 'Det är gjort. Turning Torso pekar rakt ned nu. Havet är stilla. Alltför stilla.', kind: 'system' });
  } else if (s.hemlighet <= 0) {
    s.phase = 'lost';
    s.lostReason = 'Utredarna slog till vid gryningen. Källaren är tom. Mattan ligger kvar. Under mattan finns fortfarande bordet.';
    pushLog(s, { tick: s.tick, text: s.lostReason, kind: 'system' });
  }

  return s;
}

const MAX_OFFLINE_SECONDS = 24 * 3600; // cap the catch-up so a week-long absence doesn't insta-win or insta-lose
const MIN_OFFLINE_SECONDS = 60; // don't bother with a log line for a quick tab-switch

// Approximates what `tick()` would have done, second-by-second, without
// actually running potentially hundreds of thousands of frames synchronously
// on load. Uses current rates as a flat average over the away period — close
// enough for a narrative idle game, not meant to be exact.
export function applyOfflineProgress(state: State, awayMs: number): State {
  if (state.phase !== 'playing') return state;
  const awaySec = Math.min(MAX_OFFLINE_SECONDS, awayMs / 1000);
  if (awaySec < MIN_OFFLINE_SECONDS) return state;

  const s = { ...state, areas: { ...state.areas }, upgrades: new Set(state.upgrades), log: [...state.log], usedEventIds: new Set(state.usedEventIds), flags: new Set(state.flags) };

  s.andakt += s.andaktRate * s.troende * awaySec;
  s.inflytande += INFLYTANDE_BASE_RATE * s.inflytandeRate * Math.pow(s.troende, 1.2) * awaySec;
  s.dread += s.dreadRate * awaySec;
  if (s.ritualUnlocked) s.ritual = Math.min(100, s.ritual + s.ritualRate * awaySec);

  const kaosScale = 1 + (s.dread / 200);
  const kaosGain = s.kaosRate * kaosScale * awaySec;
  const kaosReleased = s.kaos + kaosGain >= 100;
  s.kaos = Math.min(100, s.kaos + kaosGain);

  const troendeDrain = s.troende > 4 ? HEMLIGHET_DRAIN_PER_TROENDE * (s.troende - 4) : 0;
  const dreadDrain = DREAD_HEMLIGHET_DRAIN * (s.dread / (s.dread + 150));
  const ritualDrain = s.ritualUnlocked ? RITUAL_HEMLIGHET_DRAIN * (s.ritual / 100) : 0;
  s.hemlighet = Math.max(0, s.hemlighet - (troendeDrain + dreadDrain + ritualDrain) * awaySec);

  // A little flavor for the time away — non-choice events only, since nobody
  // was there to answer. Roughly one per two hours away, capped low so the
  // log doesn't get swamped after a long absence.
  const eventRolls = Math.min(3, Math.floor(awaySec / 7200));
  for (let i = 0; i < eventRolls; i++) {
    const ev = pickEvent(STANDARD_EVENTS.filter(e => !e.choices?.length), s);
    if (!ev) break;
    s.usedEventIds.add(ev.id);
    applyEffect(s, ev.effect);
  }
  if (kaosReleased) {
    s.kaos = Math.max(0, s.kaos - 80);
  }

  const awayLabel =
    awaySec < 3600 ? `${Math.round(awaySec / 60)} minuter` :
    awaySec < MAX_OFFLINE_SECONDS ? `${(awaySec / 3600).toFixed(1)} timmar` :
    'länge — för länge för att räkna';
  const summary =
    awaySec < 3600 ? `Ni var borta i ${awayLabel}. Källaren väntade, otålig.` :
    awaySec < 6 * 3600 ? `Ni var borta i ${awayLabel}. Andakten samlades utan er.` :
    awaySec < MAX_OFFLINE_SECONDS ? `Ni var borta i ${awayLabel}. Dread steg tyst i källaren medan ni sov.` :
    `Ni var borta ${awayLabel}. Källaren har fortsatt utan er. Det är oklart hur.`;
  pushLog(s, { tick: s.tick, text: summary, kind: 'system' });

  if (s.hemlighet <= 0) {
    s.phase = 'lost';
    s.lostReason = 'Utredarna slog till medan ni var borta. Källaren är tom. Mattan ligger kvar. Under mattan finns fortfarande bordet.';
    pushLog(s, { tick: s.tick, text: s.lostReason, kind: 'system' });
  } else if (s.ritual >= 100) {
    s.phase = 'won';
    pushLog(s, { tick: s.tick, text: 'Det är gjort — utan er i rummet. Turning Torso pekar rakt ned nu. Havet är stilla. Alltför stilla.', kind: 'system' });
  }

  return s;
}

export function resolveEventChoice(state: State, choiceIndex: number): State {
  if (!state.pendingEvent) return state;
  const choice = state.pendingEvent.event.choices?.[choiceIndex];
  if (!choice) return state;

  const s = { ...state, areas: { ...state.areas }, upgrades: new Set(state.upgrades), log: [...state.log], usedEventIds: new Set(state.usedEventIds), flags: new Set(state.flags) };
  applyEffect(s, choice.effect);
  if (choice.flag) s.flags.add(choice.flag);
  pushLog(s, { tick: s.tick, text: `↳ ${choice.resultText}`, kind: s.pendingEvent!.kind });
  s.pendingEvent = null;

  if (s.hemlighet <= 0) {
    s.phase = 'lost';
    s.lostReason = 'Utredarna slog till vid gryningen. Källaren är tom. Mattan ligger kvar. Under mattan finns fortfarande bordet.';
    pushLog(s, { tick: s.tick, text: s.lostReason, kind: 'system' });
  } else if (s.ritual >= 100) {
    s.phase = 'won';
    pushLog(s, { tick: s.tick, text: 'Det är gjort. Turning Torso pekar rakt ned nu. Havet är stilla. Alltför stilla.', kind: 'system' });
  }

  return s;
}

interface LocationVisit {
  texts: string[];
  effect: Partial<{ andakt: number; dread: number; kaos: number; ritual: number; hemlighet: number }>;
  requiresArea?: keyof State['areas'];
  lockedText: string;
}

const LOCATION_VISITS: Record<string, LocationVisit> = {
  rosengard: {
    texts: [
      'Du sitter i källaren. Lamporna flimrar i takt med ditt hjärta. Eller tvärtom. +andakt',
      'Någon har städat källaren medan ingen var där. Ingen av er erkänner sig skyldig. +andakt',
      'Den varma stolen är varm igen. Ingen har suttit i den. +andakt',
      'Hyresvärden har inte hört av sig på tre månader. Hyran dras ändå, varje månad, på utsatt dag. +andakt',
      'Det droppar från taket på exakt samma ställe som förra veckan. Golvet under är torrt. +andakt',
    ],
    effect: { andakt: 15 },
    requiresArea: 'rosengard',
    lockedText: 'En fuktig källarlokal i Rosengård. Du kan se ljuset under dörren. Ni är inte redo än.',
  },
  varnhem: {
    texts: [
      'Cirkeln är lite större sedan du var här sist. Du ritade den inte. +dread +ritual',
      'Kritan på asfalten har inte suddats trots regnet i natt. +dread +ritual',
      'Torget är tomt förutom en duva som går i exakt cirkelns mönster. +dread +ritual',
      'Spårvagnen bromsar mjukt precis vid cirkelns kant, varje gång, utan att föraren verkar märka det. +dread +ritual',
      'Någon har lagt blommor mitt i cirkeln. Det står inget namn på kortet. Datumet är nästa vecka. +dread +ritual',
    ],
    effect: { dread: 8, ritual: 2 },
    requiresArea: 'varnhem',
    lockedText: 'Värnhemstorget ser normalt ut. Det är ett dåligt tecken.',
  },
  torso: {
    texts: [
      'Byggnaden pekar. Du tittar dit den pekar. Du tittar bort igen. Det hjälper inte. +dread +ritual',
      'Ett fönster högt uppe är tänt som det aldrig varit förut. Ingen bor där. Ingen har någonsin bott där. +dread +ritual',
      'Torso vrider sig en grad till i vinden som inte finns. +dread +ritual',
      'Hissarna stannar på en våning som inte finns i receptionens skylt. Dörrarna öppnas ändå. Ingen går ut. +dread +ritual',
      'Fasadens glas reflekterar en himmel som inte matchar den du står under. +dread +ritual',
    ],
    effect: { dread: 15, ritual: 5 },
    requiresArea: 'torso',
    lockedText: 'Turning Torso vrider sig i vinden. Inte i vinden. I något annat.',
  },
  oresund: {
    texts: [
      'Vattnet sjunker lite långsammare just nu. Det vet att du stod här. +dread',
      'En färja passerar utan ljud. Ingen ombord vinkar. Alla ombord tittar rakt på dig. +dread',
      'Bron surrar på en frekvens du känner i tänderna snarare än öronen. +dread',
      'Fiskarna vid stranden ligger stilla i vattenbrynet, andas fortfarande, väntar på något. +dread',
      'Tullstationens skärmar visar samma bil passera om och om igen, med olika registreringsskyltar. +dread',
    ],
    effect: { dread: 5 },
    lockedText: 'Öresund. Danskarna på andra sidan verkar inte märka någonting. Det är det mest oroande av allt.',
  },
  ribersborg: {
    texts: [
      'Änderna tittar på dig. En av dem nickar. Du nickar tillbaka. Det var ett misstag. +andakt',
      'Alla änderna vänder sig samtidigt mot Turning Torso när du kommer. +andakt',
      'Sanden är kall trots solen. Ingen annan verkar märka det. +andakt',
      'Kallbadhusets bastu är på trots att ingen bokat den. Dörren är varm att röra vid, ingen sitter inne. +andakt',
      'En strandpromenerare frågar om vägen till en adress som inte finns i Malmö. Du visar honom ändå. +andakt',
    ],
    effect: { andakt: 8 },
    lockedText: 'Stranden är tom nu. Änderna sitter stilla. De väntar på rätt tidpunkt.',
  },
  davidshall: {
    texts: [
      'Källarlokalen luktar fortfarande. Du andas djupare än du borde. +andakt',
      'Kaklet från 1923 är kallt att röra vid, oavsett årstid. +andakt',
      'Grannen genom väggen sjunger något som nästan, men inte riktigt, är en psalm. +andakt',
      'Elmätaren i trapphuset räknar baklänges. Elbolaget har inte hört av sig om det. +andakt',
      'En tidigare hyresgästs post kommer fortfarande. Adressaten flyttade ut 1971. +andakt',
    ],
    effect: { andakt: 10 },
    lockedText: 'Davidshallsgatan. Kakel från 1923. Bakom det finns något som väljer att vänta.',
  },
  stortorget: {
    texts: [
      'Du vet inte varför du kom hit. Du vet inte hur länge du stod här. +kaos',
      'Statyn på torget har flyttat en centimeter sedan i förra veckan. Du är den enda som mätt. +kaos',
      'Alla klockor på torget visar olika tid. Ingen av dem är fel. +kaos',
      'Ett gatumusikkorband spelar samma stycke de spelade förra gången du var här. De säger att de aldrig spelat det förut. +kaos',
      'Duvorna flyger i en cirkel över torget som inte sluter sig, om och om igen, tills du slutar titta. +kaos',
    ],
    effect: { kaos: 8 },
    lockedText: 'Stortorget är fullt av folk som inte vet vad de gör här heller. Du passar in.',
  },
};

export const VISITABLE_LOCATION_IDS = Object.keys(LOCATION_VISITS);

const VISIT_COOLDOWN_TICKS = 900; // 30s at 30fps

export function visitLocation(state: State, id: string, cooldowns: Map<string, number>): { state: State; cooldowns: Map<string, number> } {
  const visit = LOCATION_VISITS[id];
  if (!visit) return { state, cooldowns };

  const now = state.tick;
  if ((cooldowns.get(id) ?? 0) > now) return { state, cooldowns };

  const s = { ...state, areas: { ...state.areas }, upgrades: new Set(state.upgrades), log: [...state.log], usedEventIds: new Set(state.usedEventIds), flags: new Set(state.flags) };

  const areaLocked = visit.requiresArea && !s.areas[visit.requiresArea];
  const text = areaLocked ? visit.lockedText : visit.texts[Math.floor(Math.random() * visit.texts.length)];

  if (!areaLocked) {
    if (visit.effect.andakt) s.andakt += visit.effect.andakt;
    if (visit.effect.dread) s.dread += visit.effect.dread;
    if (visit.effect.kaos) s.kaos = Math.min(100, s.kaos + visit.effect.kaos);
    if (visit.effect.ritual && s.ritualUnlocked) s.ritual = Math.min(100, s.ritual + visit.effect.ritual);
    if (visit.effect.hemlighet) s.hemlighet = Math.max(0, Math.min(100, s.hemlighet + visit.effect.hemlighet));
  }

  pushLog(s, { tick: now, text: `⬡ ${text}`, kind: 'system' });

  const newCooldowns = new Map(cooldowns);
  newCooldowns.set(id, now + VISIT_COOLDOWN_TICKS);

  return { state: s, cooldowns: newCooldowns };
}

export function saveState(state: State): void {
  try {
    const serial = {
      ...state,
      upgrades: [...state.upgrades],
      usedEventIds: [...state.usedEventIds],
      flags: [...state.flags],
      log: state.log.slice(0, 20),
    };
    localStorage.setItem(SAVE_KEY, JSON.stringify(serial));
  } catch (_) { /* storage full or unavailable */ }
}

export function loadState(): State | null {
  try {
    const raw = localStorage.getItem(SAVE_KEY);
    if (!raw) return null;
    const d = JSON.parse(raw);
    return {
      ...d,
      upgrades: new Set<string>(d.upgrades ?? []),
      usedEventIds: new Set<string>(d.usedEventIds ?? []),
      flags: new Set<string>(d.flags ?? []),
      logSeq: d.logSeq ?? 0,
      // Saves from before offline progress existed won't have this — treat
      // them as "just now" so an old save doesn't suddenly claim you were
      // away since the Unix epoch.
      lastRealTime: d.lastRealTime ?? Date.now(),
    } as State;
  } catch (_) {
    return null;
  }
}

export function clearSave(): void {
  localStorage.removeItem(SAVE_KEY);
}

export function buyUpgrade(state: State, id: string): State {
  const upgrade = UPGRADES.find(u => u.id === id);
  if (!upgrade) return state;
  if (!upgrade.available(state)) return state;
  const cost = upgrade.cost(state);
  if (cost.andakt && state.andakt < cost.andakt) return state;
  if (cost.dread && state.dread < cost.dread) return state;
  if (cost.inflytande && state.inflytande < cost.inflytande) return state;

  const s = { ...state, areas: { ...state.areas }, upgrades: new Set(state.upgrades), log: [...state.log], usedEventIds: new Set(state.usedEventIds), flags: new Set(state.flags) };
  if (cost.andakt) s.andakt -= cost.andakt;
  if (cost.dread) s.dread -= cost.dread;
  if (cost.inflytande) s.inflytande -= cost.inflytande;
  upgrade.effect(s);
  if (upgrade.once) s.upgrades.add(id);
  pushLog(s, { tick: s.tick, text: `↳ ${upgrade.name}: ${upgrade.desc}`, kind: 'upgrade' });
  return s;
}
