import type * as THREE from "three";
import type { GamePhase, GameplayActivity } from "../game/types";

export type SceneRenderMode = "active" | "outcome" | "static" | "suspended";
export type OutcomePhase = Extract<GamePhase, "victory" | "defeat" | "intermission">;

// Visible modes stay in R3F's demand clock domain. Suspended mode blocks loader
// and host-commit invalidations as well as the explicit gameplay request loop.
export function sceneCanvasFrameloop(mode: SceneRenderMode): "demand" | "never" {
  return mode === "suspended" ? "never" : "demand";
}

export function resetSceneClockForMode(clock: Pick<THREE.Clock, "start">, mode: SceneRenderMode) {
  if (mode === "active" || mode === "outcome") clock.start();
}

export const GAMEPLAY_FRAME_INTERVAL_MS = 1_000 / 60;
export const OUTCOME_FRAME_INTERVAL_MS = 1_000 / 30;
export const FRAME_INTERVAL_JITTER_MS = 1.5;
export const SIMULATION_STEP_SECONDS = 0.1;
export const MAX_SIMULATION_STEPS_PER_FRAME = 3;
export const MAX_FRAME_DELTA_SECONDS = 0.25;
export const OUTCOME_RENDER_TAIL_SECONDS = 7;

export interface SceneRenderState {
  phase: GamePhase;
  paused: boolean;
  visible: boolean;
  outcomeTailComplete: boolean;
}

export function isOutcomePhase(phase: GamePhase): phase is OutcomePhase {
  return phase === "victory" || phase === "defeat" || phase === "intermission";
}

export function outcomeElapsedAfterPhaseChange(
  previousPhase: OutcomePhase | null,
  nextPhase: OutcomePhase | null,
  elapsedSeconds: number,
) {
  return previousPhase === nextPhase ? Math.max(0, elapsedSeconds) : 0;
}

export function selectSceneRenderMode({
  phase,
  paused,
  visible,
  outcomeTailComplete,
}: SceneRenderState): SceneRenderMode {
  if (!visible) return "suspended";
  if (phase === "combat" && !paused) return "active";
  if (isOutcomePhase(phase) && !outcomeTailComplete) return "outcome";
  return "static";
}

export function sceneFrameIntervalMs(mode: SceneRenderMode) {
  if (mode === "active") return GAMEPLAY_FRAME_INTERVAL_MS;
  if (mode === "outcome") return OUTCOME_FRAME_INTERVAL_MS;
  return null;
}

export function sceneTargetFps(mode: SceneRenderMode) {
  if (mode === "active") return 60;
  if (mode === "outcome") return 30;
  return 0;
}

export interface SimulationStepResult {
  steps: number;
  remainderSeconds: number;
}

export function consumeSimulationSteps(
  accumulatorSeconds: number,
  elapsedSeconds: number,
): SimulationStepResult {
  const boundedElapsed = Math.min(MAX_FRAME_DELTA_SECONDS, Math.max(0, elapsedSeconds));
  let remainderSeconds = Math.max(0, accumulatorSeconds) + boundedElapsed;
  const steps = Math.min(
    MAX_SIMULATION_STEPS_PER_FRAME,
    Math.floor((remainderSeconds + Number.EPSILON) / SIMULATION_STEP_SECONDS),
  );
  remainderSeconds -= steps * SIMULATION_STEP_SECONDS;
  return { steps, remainderSeconds };
}

export interface SceneFrameSample {
  elapsedMs: number;
  elapsedSeconds: number;
  outcomeElapsedSeconds: number;
}

export interface SceneFrameLoopOptions {
  mode: "active" | "outcome";
  initialOutcomeElapsedSeconds?: number;
  requestFrame: (callback: FrameRequestCallback) => number;
  cancelFrame: (handle: number) => void;
  onFrame: (sample: SceneFrameSample) => void;
  onOutcomeComplete?: () => void;
}

/**
 * Starts the bounded request loop used for active gameplay and finite outcome
 * animation tails. Static and suspended modes intentionally have no loop.
 */
export function startSceneFrameLoop({
  mode,
  initialOutcomeElapsedSeconds = 0,
  requestFrame,
  cancelFrame,
  onFrame,
  onOutcomeComplete,
}: SceneFrameLoopOptions) {
  const intervalMs = sceneFrameIntervalMs(mode)!;
  let frameHandle: number | null = null;
  let stopped = false;
  let lastRenderedAt: number | null = null;
  let outcomeElapsedSeconds = mode === "outcome" ? Math.max(0, initialOutcomeElapsedSeconds) : 0;

  const scheduleNext = () => {
    frameHandle = requestFrame(runFrame);
  };

  const runFrame: FrameRequestCallback = (now) => {
    if (stopped) return;

    const previous = lastRenderedAt;
    if (previous === null) {
      lastRenderedAt = now;
      onFrame({ elapsedMs: 0, elapsedSeconds: 0, outcomeElapsedSeconds });
    } else {
      const elapsedMs = now - previous;
      if (elapsedMs + FRAME_INTERVAL_JITTER_MS >= intervalMs) {
        lastRenderedAt = now;
        const wallElapsedSeconds = Math.max(0, elapsedMs / 1_000);
        const elapsedSeconds = mode === "active"
          ? Math.min(MAX_FRAME_DELTA_SECONDS, wallElapsedSeconds)
          : wallElapsedSeconds;
        if (mode === "outcome") outcomeElapsedSeconds += wallElapsedSeconds;
        onFrame({ elapsedMs, elapsedSeconds, outcomeElapsedSeconds });

        if (mode === "outcome" && outcomeElapsedSeconds >= OUTCOME_RENDER_TAIL_SECONDS) {
          stopped = true;
          frameHandle = null;
          onOutcomeComplete?.();
          return;
        }
      }
    }

    scheduleNext();
  };

  scheduleNext();

  return () => {
    stopped = true;
    if (frameHandle !== null) cancelFrame(frameHandle);
    frameHandle = null;
  };
}

export type ActivePlayfieldKind = Exclude<GameplayActivity, "boss">;

export function activePlayfieldKind(activity: GameplayActivity): ActivePlayfieldKind | null {
  return activity === "boss" ? null : activity;
}

export interface FramePerformanceSummary {
  mode: SceneRenderMode;
  targetFps: number;
  budgetMs: number | null;
  averageMs: number;
  p95Ms: number;
  p99Ms: number;
  overBudget: number;
  samples: number;
}

function percentile(sorted: readonly number[], ratio: number) {
  if (!sorted.length) return 0;
  return sorted[Math.min(sorted.length - 1, Math.floor(sorted.length * ratio))];
}

export function summarizeFramePerformance(
  mode: SceneRenderMode,
  samples: readonly number[],
): FramePerformanceSummary {
  const targetFps = sceneTargetFps(mode);
  const budgetMs = targetFps > 0 ? 1_000 / targetFps : null;
  const sorted = [...samples].sort((left, right) => left - right);
  let total = 0;
  let overBudget = 0;
  for (const duration of samples) {
    total += duration;
    if (mode === "active" && budgetMs !== null && duration > budgetMs + FRAME_INTERVAL_JITTER_MS) overBudget += 1;
  }
  return {
    mode,
    targetFps,
    budgetMs,
    averageMs: samples.length ? total / samples.length : 0,
    p95Ms: percentile(sorted, 0.95),
    p99Ms: percentile(sorted, 0.99),
    overBudget,
    samples: samples.length,
  };
}
