import {
  AWARENESS_MAX,
  AWARENESS_START,
  CLUE_AWARENESS,
  DEFAULT_PLAYER_NAME,
  DIALOG_GENTLE_HAPPINESS,
  DIALOG_HESITANT_AWARENESS,
  FORCED_CELEBRATION_MS,
  FORCED_CELEBRATION_RESTORE,
  FORCED_CELEBRATION_THRESHOLD,
  HAPPINESS_MAX,
  HAPPINESS_MIN,
  HAPPINESS_START,
  MICRO_DAILY_CAP,
  MICRO_HAPPINESS,
  OVERFLOW_POOL_MAX,
  SADNESS_AWARENESS,
  SADNESS_HAPPINESS,
  WELL_FOG_GAZE_AWARENESS,
} from '../data/constants';
import { getAfuLine } from '../data/afuLines';
import type { ClueId } from '../data/clues';
import { C9_DAYS, C9_LINES } from '../data/clues';
import {
  faceTierForNpc,
  petalCount,
  showBlackSeeds,
  stageFromHappiness,
  stageParams,
  type FaceTier,
  type Stage,
  type StageParams,
} from '../data/corruption';
import { evaluateEnding, type EndingId } from '../data/endings';
import type { NpcId } from '../data/npcs';
import { getNpc } from '../data/npcs';
import { getTask, getTasksForDay, type TaskId } from '../data/tasks';
import { gameEvents } from './EventBus';

export type MicroType = 'water' | 'feed' | 'pet';
export type DialogChoice = 'gentle' | 'hesitant' | 'negate';
export type SadnessKind =
  | 'refuse_task'
  | 'negate_dialog'
  | 'idle'
  | 'entangle'
  | 'rain';

export interface DirectorSnapshot {
  playerName: string;
  happiness: number;
  awareness: number;
  day: number;
  stage: Stage;
  stageParams: StageParams;
  petals: number;
  blackSeeds: boolean;
  clues: ClueId[];
  completedTasks: TaskId[];
  refusedTasks: TaskId[];
  celebrationEscapes: number;
  overflowPool: number;
  sessionStartMs: number;
  dayPhase: DayPhase;
  c9Step: number;
  lowHappinessSinceMs: number | null;
  forcedCelebrationActive: boolean;
  rainActive: boolean;
  duskReached: boolean;
  boardReadToday: boolean;
  microCounts: Record<MicroType, number>;
  npcTalkCounts: Record<string, number>;
  /** Inventory: persists across scene travel (not scene-local) */
  hasBread: boolean;
  hasLetters: boolean;
  breadDelivered: string[];
  lettersDelivered: string[];
  waterCount: number;
  greetCount: number;
  berryCount: number;
  ribbonCount: number;
  lowHappinessD5to7: boolean;
  wellGazeToday: boolean;
  fogGazeToday: boolean;
  t14Active: boolean;
  t14ElapsedMs: number;
}

export type DayPhase = 'morning' | 'day' | 'dusk' | 'sleep' | 'dream';

class CorruptionDirectorImpl {
  playerName = DEFAULT_PLAYER_NAME;
  happiness = HAPPINESS_START;
  awareness = AWARENESS_START;
  day = 1;
  clues = new Set<ClueId>();
  completedTasks = new Set<TaskId>();
  refusedTasks = new Set<TaskId>();
  celebrationEscapes = 0;
  overflowPool = 0;
  sessionStartMs = Date.now();
  dayPhase: DayPhase = 'morning';
  c9Step = 0; // 0,1,2 completed steps
  lowHappinessSinceMs: number | null = null;
  forcedCelebrationActive = false;
  rainActive = false;
  duskReached = false;
  boardReadToday = false;
  microCounts: Record<MicroType, number> = { water: 0, feed: 0, pet: 0 };
  npcTalkCounts: Record<string, number> = {};
  /** Carrying honey bread / mail — survives map transitions */
  hasBread = false;
  hasLetters = false;
  breadDelivered: string[] = [];
  lettersDelivered: string[] = [];
  waterCount = 0;
  greetCount = 0;
  berryCount = 0;
  ribbonCount = 0;
  /** Tracks if happiness stayed ≤10 continuously through D5–D7 */
  lowHappinessD5to7 = true;
  hadHighHappinessD5to7 = false;
  wellGazeToday = false;
  fogGazeToday = false;
  t14Active = false;
  t14ElapsedMs = 0;
  sentenceCounter = 0;
  lastNearestNpcLook: NpcId | null = null;
  ended = false;
  endingId: EndingId | null = null;
  choseFogWall = false;

  /** Real-time clock for forced celebration (injectable for tests) */
  nowMs = (): number => Date.now();

  resetSession(name?: string): void {
    this.playerName = name?.trim() || DEFAULT_PLAYER_NAME;
    this.happiness = HAPPINESS_START;
    this.awareness = AWARENESS_START;
    this.day = 1;
    this.clues.clear();
    this.completedTasks.clear();
    this.refusedTasks.clear();
    this.celebrationEscapes = 0;
    this.overflowPool = 0;
    this.sessionStartMs = this.nowMs();
    this.dayPhase = 'morning';
    this.c9Step = 0;
    this.lowHappinessSinceMs = null;
    this.forcedCelebrationActive = false;
    this.rainActive = false;
    this.duskReached = false;
    this.boardReadToday = false;
    this.microCounts = { water: 0, feed: 0, pet: 0 };
    this.npcTalkCounts = {};
    this.hasBread = false;
    this.hasLetters = false;
    this.breadDelivered = [];
    this.lettersDelivered = [];
    this.waterCount = 0;
    this.greetCount = 0;
    this.berryCount = 0;
    this.ribbonCount = 0;
    this.lowHappinessD5to7 = true;
    this.hadHighHappinessD5to7 = false;
    this.wellGazeToday = false;
    this.fogGazeToday = false;
    this.t14Active = false;
    this.t14ElapsedMs = 0;
    this.sentenceCounter = 0;
    this.lastNearestNpcLook = null;
    this.ended = false;
    this.endingId = null;
    this.choseFogWall = false;
    this.emitAll();
  }

  get stage(): Stage {
    return stageFromHappiness(this.happiness);
  }

  getStageParams(): StageParams {
    return stageParams(this.stage);
  }

  getPetals(): number {
    return petalCount(this.happiness);
  }

  getBlackSeeds(): boolean {
    return showBlackSeeds(this.happiness);
  }

  getAfuLine(): string {
    return getAfuLine(this.day);
  }

  getTodayTasks() {
    return getTasksForDay(this.day);
  }

  sessionDurationMs(): number {
    return this.nowMs() - this.sessionStartMs;
  }

  sessionDurationLabel(): string {
    const ms = this.sessionDurationMs();
    const totalSec = Math.floor(ms / 1000);
    const m = Math.floor(totalSec / 60);
    const s = totalSec % 60;
    return `${m}分${s.toString().padStart(2, '0')}秒`;
  }

  setHappiness(value: number, opts: { force?: boolean } = {}): void {
    const clamped = Math.max(HAPPINESS_MIN, Math.min(HAPPINESS_MAX, Math.round(value)));
    this.happiness = clamped;
    if (this.day >= 5 && this.day <= 7 && this.happiness > 10) {
      this.hadHighHappinessD5to7 = true;
      this.lowHappinessD5to7 = false;
    }
    this.syncLowHappinessTimer();
    if (!opts.force && this.happiness < 100) {
      this.overflowPool = 0;
    }
    gameEvents.emit('happiness', this.happiness);
    gameEvents.emit('stage', this.stage, this.getStageParams());
    gameEvents.emit('petals', this.getPetals(), this.getBlackSeeds());
  }

  addHappiness(delta: number): void {
    if (delta === 0) return;
    if (delta > 0) {
      const next = Math.min(HAPPINESS_MAX, this.happiness + delta);
      this.setHappiness(next);
      return;
    }
    // Negative
    if (this.happiness >= 100) {
      // Overflow pool absorbs; small deductions don't lower happiness
      this.overflowPool += Math.abs(delta);
      if (this.overflowPool >= OVERFLOW_POOL_MAX) {
        this.overflowPool = 0;
        gameEvents.emit('negativeFlash');
        // Stay at S4 / 100
      }
      gameEvents.emit('overflow', this.overflowPool);
      return;
    }
    this.setHappiness(this.happiness + delta);
  }

  setAwareness(value: number): void {
    const next = Math.max(this.awareness, Math.min(AWARENESS_MAX, Math.round(value)));
    if (next === this.awareness) return;
    this.awareness = next;
    gameEvents.emit('awareness', this.awareness);
  }

  addAwareness(delta: number): void {
    if (delta <= 0) return; // never decreases
    this.setAwareness(this.awareness + delta);
  }

  private syncLowHappinessTimer(): void {
    if (this.happiness < FORCED_CELEBRATION_THRESHOLD) {
      if (this.lowHappinessSinceMs == null) {
        this.lowHappinessSinceMs = this.nowMs();
      }
    } else {
      this.lowHappinessSinceMs = null;
    }
  }

  /** Call from game loop with real dt */
  tick(dtMs: number): void {
    if (this.ended) return;
    // Forced celebration check
    if (
      !this.forcedCelebrationActive &&
      this.happiness < FORCED_CELEBRATION_THRESHOLD &&
      this.lowHappinessSinceMs != null
    ) {
      if (this.nowMs() - this.lowHappinessSinceMs >= FORCED_CELEBRATION_MS) {
        this.triggerForcedCelebration();
      }
    }
    if (this.t14Active) {
      this.t14ElapsedMs += dtMs;
      gameEvents.emit('t14Progress', this.t14ElapsedMs);
    }
  }

  triggerForcedCelebration(): void {
    this.forcedCelebrationActive = true;
    this.setHappiness(FORCED_CELEBRATION_RESTORE);
    this.lowHappinessSinceMs = null;
    gameEvents.emit('forcedCelebration', true);
  }

  escapeForcedCelebration(): void {
    if (!this.forcedCelebrationActive) return;
    this.forcedCelebrationActive = false;
    this.celebrationEscapes += 1;
    gameEvents.emit('forcedCelebration', false);
    gameEvents.emit('celebrationEscape', this.celebrationEscapes);
  }

  endForcedCelebration(): void {
    this.forcedCelebrationActive = false;
    gameEvents.emit('forcedCelebration', false);
  }

  applySadness(kind: SadnessKind, nearestNpcId?: NpcId | null): void {
    this.addHappiness(SADNESS_HAPPINESS);
    this.addAwareness(SADNESS_AWARENESS);
    if (nearestNpcId) {
      this.lastNearestNpcLook = nearestNpcId;
      gameEvents.emit('npcLookAtPlayer', nearestNpcId);
    } else {
      gameEvents.emit('npcLookAtPlayer', null);
    }
    gameEvents.emit('sadness', kind);
  }

  discoverClue(id: ClueId): boolean {
    if (this.clues.has(id)) return false;
    // Day gates
    if (id === 'C1' && this.day < 2) return false;
    if (id === 'C2' && this.day < 3) return false;
    if (id === 'C3' && this.day < 2) return false;
    if (id === 'C5' && this.day < 2) return false;
    if (id === 'C6' && this.day < 5) return false;
    if (id === 'C7' && this.day < 5) return false;
    if (id === 'C8' && this.day < 5) return false;
    this.clues.add(id);
    this.addAwareness(CLUE_AWARENESS);
    gameEvents.emit('clue', id);
    return true;
  }

  grantAllClues(): void {
    const all: ClueId[] = ['C1', 'C2', 'C3', 'C4', 'C5', 'C6', 'C7', 'C8', 'C9'];
    for (const id of all) {
      if (!this.clues.has(id)) {
        this.clues.add(id);
        this.addAwareness(CLUE_AWARENESS);
      }
    }
    this.c9Step = 3;
    gameEvents.emit('clue', 'ALL');
  }

  /** C9 progressive steps on D2/D4/D6 */
  advanceC9(): string | null {
    const expectedDay = C9_DAYS[this.c9Step];
    if (expectedDay == null) return null;
    if (this.day !== expectedDay && this.day < expectedDay) return null;
    // Allow if day matches step day or later for that step (but each step once)
    if (this.day < expectedDay) return null;
    const line = C9_LINES[this.c9Step]!;
    this.c9Step += 1;
    if (this.c9Step >= 3) {
      this.discoverClue('C9');
    }
    return line;
  }

  getC9LineForTalk(): string | null {
    // Return next available progressive line if day matches
    if (this.c9Step >= 3) return C9_LINES[2]!;
    const dayForStep = C9_DAYS[this.c9Step];
    if (dayForStep != null && this.day >= dayForStep) {
      return this.advanceC9();
    }
    return null;
  }

  completeTask(id: TaskId): boolean {
    if (this.completedTasks.has(id)) return false;
    const task = getTask(id);
    if (task.day !== this.day && id !== 'T15') {
      // Allow debug / flexible only same day for normal play
      // Keep strict for day match
      if (task.day !== this.day) return false;
    }
    this.completedTasks.add(id);
    if (task.happinessReward > 0) {
      this.addHappiness(task.happinessReward);
    }
    gameEvents.emit('taskComplete', id, task.happinessReward);
    this.checkDuskByTasks();
    return true;
  }

  refuseTask(id: TaskId): boolean {
    const task = getTask(id);
    if (!task.refuseable) return false;
    if (this.refusedTasks.has(id) || this.completedTasks.has(id)) return false;
    this.refusedTasks.add(id);
    this.applySadness('refuse_task');
    gameEvents.emit('taskRefuse', id);
    return true;
  }

  private checkDuskByTasks(): void {
    const today = getTasksForDay(this.day).filter((t) => t.id !== 'T15');
    if (today.length === 0) return;
    const allDone = today.every(
      (t) => this.completedTasks.has(t.id) || this.refusedTasks.has(t.id),
    );
    if (allDone && !this.duskReached) {
      this.triggerDusk();
    }
  }

  triggerDusk(): void {
    if (this.duskReached) return;
    this.duskReached = true;
    this.dayPhase = 'dusk';
    if (this.day === 6) {
      this.rainActive = true;
      gameEvents.emit('rain', true);
    }
    gameEvents.emit('dusk');
  }

  goToSleep(): void {
    this.dayPhase = 'sleep';
    gameEvents.emit('sleep');
  }

  advanceDay(): void {
    if (this.day >= 7) return;
    this.day += 1;
    this.dayPhase = 'morning';
    this.duskReached = false;
    this.boardReadToday = false;
    this.microCounts = { water: 0, feed: 0, pet: 0 };
    this.npcTalkCounts = {};
    this.hasBread = false;
    this.hasLetters = false;
    this.breadDelivered = [];
    this.lettersDelivered = [];
    this.waterCount = 0;
    this.greetCount = 0;
    this.berryCount = 0;
    this.ribbonCount = 0;
    this.wellGazeToday = false;
    this.fogGazeToday = false;
    this.t14Active = false;
    this.t14ElapsedMs = 0;
    this.rainActive = false;
    if (this.day >= 5 && this.day <= 7) {
      if (this.happiness > 10) {
        this.lowHappinessD5to7 = false;
        this.hadHighHappinessD5to7 = true;
      }
    }
    gameEvents.emit('day', this.day);
    gameEvents.emit('rain', false);
  }

  setDay(day: number): void {
    this.day = Math.max(1, Math.min(7, Math.round(day)));
    gameEvents.emit('day', this.day);
  }

  readBoard(): void {
    this.boardReadToday = true;
    this.dayPhase = 'day';
    gameEvents.emit('boardRead', this.getAfuLine());
  }

  dialogChoice(choice: DialogChoice, npcId?: NpcId): void {
    if (npcId) {
      this.npcTalkCounts[npcId] = (this.npcTalkCounts[npcId] ?? 0) + 1;
      if (this.npcTalkCounts[npcId]! >= 5) {
        this.applySadness('entangle', npcId);
      }
    }
    if (choice === 'gentle') {
      this.addHappiness(DIALOG_GENTLE_HAPPINESS);
    } else if (choice === 'hesitant') {
      this.addAwareness(DIALOG_HESITANT_AWARENESS);
    } else if (choice === 'negate') {
      this.applySadness('negate_dialog', npcId);
    }
    this.sentenceCounter += 1;
  }

  microInteract(type: MicroType): boolean {
    if (this.microCounts[type] >= MICRO_DAILY_CAP) return false;
    this.microCounts[type] += 1;
    this.addHappiness(MICRO_HAPPINESS);
    gameEvents.emit('micro', type);
    return true;
  }

  /** Gaze well or fog 5s — once each per day, +3 awareness */
  gazeWellOrFog(kind: 'well' | 'fog'): boolean {
    if (kind === 'well') {
      if (this.wellGazeToday) return false;
      this.wellGazeToday = true;
    } else {
      if (this.fogGazeToday) return false;
      this.fogGazeToday = true;
    }
    this.addAwareness(WELL_FOG_GAZE_AWARENESS);
    return true;
  }

  canInteractWellOrFog(): boolean {
    return this.day >= 5;
  }

  faceTier(npcId: NpcId): FaceTier {
    const npc = getNpc(npcId);
    return faceTierForNpc(this.happiness, npc.offset);
  }

  nextSentenceIndex(): number {
    return this.sentenceCounter++;
  }

  // --- Task progress helpers (inventory survives scene travel) ---
  takeBread(): boolean {
    if (this.day !== 1 && this.day !== 4) return false;
    const taskId = this.day === 4 ? 'T8' : 'T1';
    if (this.completedTasks.has(taskId as TaskId)) return false;
    this.hasBread = true;
    gameEvents.emit('inventory', 'bread', true);
    return true;
  }

  takeLetters(): boolean {
    if (this.day !== 2 && this.day !== 4) return false;
    // D2 T4; D4 has no letter task but keep day2-only for T4
    if (this.day === 2 && this.completedTasks.has('T4')) return false;
    if (this.day !== 2) return false;
    this.hasLetters = true;
    gameEvents.emit('inventory', 'letters', true);
    return true;
  }

  deliverBread(to: string): boolean {
    if (!this.hasBread) return false;
    if (!this.breadDelivered.includes(to)) this.breadDelivered.push(to);
    if (this.breadDelivered.length >= 3) {
      this.hasBread = false;
      const id = this.day === 4 ? 'T8' : 'T1';
      if (!this.completedTasks.has(id as TaskId)) this.completeTask(id as TaskId);
      return true;
    }
    return true;
  }

  waterFlower(): void {
    this.waterCount += 1;
    this.microInteract('water');
    if (this.waterCount >= 3) {
      const id = this.day === 4 ? 'T9' : 'T2';
      if (!this.completedTasks.has(id as TaskId)) this.completeTask(id as TaskId);
    }
  }

  greetVillager(): void {
    this.greetCount += 1;
    if (this.greetCount >= 3) {
      const id = this.day === 4 ? 'T10' : 'T3';
      if (!this.completedTasks.has(id as TaskId)) this.completeTask(id as TaskId);
    }
  }

  deliverLetter(to: string): boolean {
    if (!this.hasLetters) return false;
    if (!this.lettersDelivered.includes(to)) this.lettersDelivered.push(to);
    if (this.lettersDelivered.length === 3) {
      this.discoverClue('C4');
    }
    if (this.lettersDelivered.length >= 3) {
      this.hasLetters = false;
      this.completeTask('T4');
    }
    return true;
  }

  pickBerry(): void {
    this.berryCount += 1;
    if (this.berryCount >= 5) this.completeTask('T6');
  }

  hangRibbon(): void {
    this.ribbonCount += 1;
    if (this.ribbonCount >= 4) this.completeTask('T13');
  }

  startT14(): void {
    this.t14Active = true;
    this.t14ElapsedMs = 0;
    gameEvents.emit('t14Start');
  }

  finishT14(): void {
    this.t14Active = false;
    this.completeTask('T14');
    gameEvents.emit('t14End');
  }

  startEnding(choseFogWall = false): EndingId {
    this.choseFogWall = choseFogWall;
    if (this.day >= 5 && this.day <= 7 && this.hadHighHappinessD5to7) {
      this.lowHappinessD5to7 = false;
    }
    // Continuously ≤10 means never went above 10 on D5–7
    const lowOk = this.lowHappinessD5to7 && !this.hadHighHappinessD5to7;
    const id = evaluateEnding({
      awareness: this.awareness,
      happiness: this.happiness,
      clueCount: this.clues.size,
      lowHappinessStreakD5to7: lowOk,
      celebrationEscapes: this.celebrationEscapes,
      choseFogWall,
    });
    this.endingId = id;
    this.ended = true;
    this.completeTask('T15');
    gameEvents.emit('ending', id);
    return id;
  }

  forceEnding(id: EndingId): void {
    this.endingId = id;
    this.ended = true;
    gameEvents.emit('ending', id);
  }

  snapshot(): DirectorSnapshot {
    return {
      playerName: this.playerName,
      happiness: this.happiness,
      awareness: this.awareness,
      day: this.day,
      stage: this.stage,
      stageParams: this.getStageParams(),
      petals: this.getPetals(),
      blackSeeds: this.getBlackSeeds(),
      clues: [...this.clues],
      completedTasks: [...this.completedTasks],
      refusedTasks: [...this.refusedTasks],
      celebrationEscapes: this.celebrationEscapes,
      overflowPool: this.overflowPool,
      sessionStartMs: this.sessionStartMs,
      dayPhase: this.dayPhase,
      c9Step: this.c9Step,
      lowHappinessSinceMs: this.lowHappinessSinceMs,
      forcedCelebrationActive: this.forcedCelebrationActive,
      rainActive: this.rainActive,
      duskReached: this.duskReached,
      boardReadToday: this.boardReadToday,
      microCounts: { ...this.microCounts },
      npcTalkCounts: { ...this.npcTalkCounts },
      hasBread: this.hasBread,
      hasLetters: this.hasLetters,
      breadDelivered: [...this.breadDelivered],
      lettersDelivered: [...this.lettersDelivered],
      waterCount: this.waterCount,
      greetCount: this.greetCount,
      berryCount: this.berryCount,
      ribbonCount: this.ribbonCount,
      lowHappinessD5to7: this.lowHappinessD5to7,
      wellGazeToday: this.wellGazeToday,
      fogGazeToday: this.fogGazeToday,
      t14Active: this.t14Active,
      t14ElapsedMs: this.t14ElapsedMs,
    };
  }

  private emitAll(): void {
    gameEvents.emit('happiness', this.happiness);
    gameEvents.emit('awareness', this.awareness);
    gameEvents.emit('day', this.day);
    gameEvents.emit('stage', this.stage, this.getStageParams());
    gameEvents.emit('petals', this.getPetals(), this.getBlackSeeds());
  }
}

export const Director = new CorruptionDirectorImpl();
export type CorruptionDirector = CorruptionDirectorImpl;
