import type { DB } from "@/db/schema";
import type { PauseType } from "@/services/video-processing-service";
import type { ClipZoomType } from "@/features/videos/clip-zoom";
import type { SilenceLength } from "@/silence-detection-constants";
import type { BrowserLinkEvent, CapturedWebLink } from "@/lib/clip-web-link";
import type { Brand } from "./utils";

export type DatabaseId = Brand<string, "DatabaseId">;
export type FrontendId = Brand<string, "FrontendId">;
export type FrontendInsertionPoint =
  | {
      type: "start";
    }
  | {
      type: "after-clip";
      frontendClipId: FrontendId;
    }
  | {
      type: "after-chapter";
      frontendChapterId: FrontendId;
    }
  | {
      type: "end";
    };

export type ClipOnDatabase = {
  type: "on-database";
  frontendId: FrontendId;
  databaseId: DatabaseId;
  videoFilename: string;
  sourceStartTime: number; // Start time in source video (seconds)
  sourceEndTime: number; // End time in source video (seconds)
  text: string;
  transcribedAt: Date | null;
  scene: string | null;
  profile: string | null;
  insertionOrder: number | null;
  pauseType: PauseType;
  /**
   * Clip Zoom. Only a persisted clip carries one — it is an editorial choice
   * about the shot, made after the take, not something a recording produces.
   */
  zoomType: ClipZoomType;
  diagramSnapshotId: string | null;
  diagramName: string | null;
  /**
   * Web pages that were on screen (focused Chrome window) while this clip was
   * recorded. Loaded from the DB for existing clips, and filled in after a
   * freshly-recorded clip's captured links are persisted.
   */
  webLinks: DB.ClipWebLink[];
  /**
   * If true, this clip has been archived by the user (deleted from session panel).
   * The clip stays in state so it can appear in the archived sub-section of the
   * session panel, showing its transcript text.
   */
  shouldArchive?: boolean;
  /**
   * The recording session this clip belongs to, inherited from the optimistic
   * clip it was paired with — so a clip keeps its session across both halves of
   * its life. Absent for clips loaded from the database on page load, which
   * belong to no session in this window.
   *
   * Read by the session panel, to group archived clips under the right session,
   * and by `session-clip-marks.ts`, which needs live clips too.
   */
  sessionId?: SessionId;
};

export type ClipOptimisticallyAdded = {
  type: "optimistically-added";
  frontendId: FrontendId;
  scene: string;
  profile: string;
  /**
   * An integer, incremented each time a new optimistically added clip is added.
   * Used to determine which clips should be paired with which database clips,
   * and to handle the deletion of the latest inserted clip.
   */
  insertionOrder: number;
  /**
   * If true, when the optimistically added clip is replaced with the database clip,
   * the clip will be archived. Allows the user to delete the clip before it's transcribed.
   */
  shouldArchive?: boolean;
  pauseType: PauseType;
  /**
   * Unique ID for the sound detection that triggered this clip.
   * Used for deduplication - prevents duplicate clips from React StrictMode double-firing.
   */
  soundDetectionId: string;
  /**
   * The recording session this clip belongs to.
   * Used to group optimistic clips in per-session UI panels.
   */
  sessionId: SessionId;
  /**
   * If true, this clip has been marked as orphaned — no matching DB clip
   * will arrive. Set by the session-polling-complete action after the session timeout fires.
   */
  isOrphaned?: boolean;
  /**
   * Diagram state captured when the clip's audio window closed (silence
   * detected). Decided once, at the moment the clip ends — not at DB pairing
   * time — so focus that drifts away during polling lag does not invalidate
   * the snapshot decision. Read when the optimistic clip is paired with a
   * database clip to decide whether to auto-pin a diagram snapshot.
   */
  pendingSnapshot?: {
    activeDiagramId: string | null;
    diagramFocused: boolean;
  };
  /**
   * Web links that were on screen during this clip, captured when the clip's
   * audio window closed. Read when the optimistic clip is paired with a database
   * clip, at which point they are persisted as `clip_web_link` rows.
   */
  pendingWebLinks?: CapturedWebLink[];
};

export const createFrontendId = (): FrontendId => {
  return crypto.randomUUID() as FrontendId;
};

export type ClipEffectOptimisticallyAdded = {
  type: "effect-clip-optimistically-added";
  frontendId: FrontendId;
  videoFilename: string;
  sourceStartTime: number;
  sourceEndTime: number;
  text: string;
  scene: string;
  profile: string;
  pauseType: PauseType;
  insertionOrder: number;
};

export type Clip =
  ClipOnDatabase | ClipOptimisticallyAdded | ClipEffectOptimisticallyAdded;

export type ChapterOnDatabase = {
  type: "chapter-on-database";
  frontendId: FrontendId;
  databaseId: DatabaseId;
  name: string;
  insertionOrder: number | null;
};

export type ChapterOptimisticallyAdded = {
  type: "chapter-optimistically-added";
  frontendId: FrontendId;
  name: string;
  insertionOrder: number;
  shouldArchive?: boolean;
};

export type Chapter = ChapterOnDatabase | ChapterOptimisticallyAdded;

export type TimelineItem = Clip | Chapter;

export type EditorError = {
  message: string;
  effectType: string;
  timestamp: number;
};

export type SessionId = Brand<string, "SessionId">;

export const createSessionId = (): SessionId => {
  return crypto.randomUUID() as SessionId;
};

export type RecordingSessionStatus = "recording" | "polling" | "done";

export type RecordingSession = {
  id: SessionId;
  displayNumber: number;
  status: RecordingSessionStatus;
  outputPath: string;
  startedAt: number;
  silenceLength: SilenceLength;
};

export type ClipReducerState = {
  items: TimelineItem[];
  clipIdsBeingTranscribed: Set<FrontendId>;
  insertionPoint: FrontendInsertionPoint;
  insertionOrder: number;
  /**
   * When set, indicates a fatal error has occurred in the video editor.
   * The editor should display an error overlay and require a page refresh.
   */
  error: EditorError | null;
  /**
   * Recording sessions. Each session groups optimistic clips created during
   * a single recording. Session numbering resets on page reload.
   */
  sessions: RecordingSession[];
  /**
   * Live browser link-capture state, fed by `browser-event` actions from the
   * Chrome extension. `browserFocus` + `browserUrl` fold to the single web page
   * currently visible on screen; this is ambient device state, not part of the
   * saved video document. Undefined is treated as "no focus / no URL".
   */
  browserFocus?: boolean;
  browserUrl?: string | null;
  browserTitle?: string | null;
  /**
   * The web link currently being evaluated for minimum dwell time before it
   * qualifies as a captured web link. Promoted to the recording clip's
   * `pendingWebLinks` once it has been visible for at least `WEB_LINK_DWELL_MS`.
   */
  browserLinkCandidate?: {
    url: string;
    title: string | null;
    since: number;
  } | null;
  /**
   * The optimistic clip currently being recorded (set when speech is detected,
   * cleared when its audio window closes). While set, the effective visible URL
   * from each `browser-event` is accumulated into that clip's `pendingWebLinks`.
   */
  recordingClipFrontendId?: FrontendId | null;
};

export type ClipReducerAction =
  | {
      type: "recording-started";
      outputPath: string;
      silenceLength: SilenceLength;
    }
  | {
      type: "recording-stopped";
    }
  | {
      type: "session-polling-complete";
      sessionId: SessionId;
    }
  | {
      type: "new-optimistic-clip-detected";
      scene: string;
      profile: string;
      soundDetectionId: string;
    }
  | {
      type: "browser-event";
      event: BrowserLinkEvent;
    }
  | {
      type: "new-database-clips";
      clips: DB.Clip[];
      outputPath?: string;
    }
  | {
      type: "clips-deleted";
      clipIds: FrontendId[];
    }
  | {
      type: "clips-retranscribing";
      clipIds: FrontendId[];
    }
  | {
      type: "clips-transcribed";
      clips: {
        databaseId: DatabaseId;
        text: string;
      }[];
    }
  | {
      type: "set-insertion-point-after";
      clipId: FrontendId;
    }
  | {
      type: "set-insertion-point-before";
      clipId: FrontendId;
    }
  | {
      type: "delete-latest-inserted-clip";
    }
  | {
      type: "toggle-pause-at-insertion-point";
    }
  | {
      type: "toggle-pause-for-clip";
      clipId: FrontendId;
    }
  | {
      type: "toggle-zoom-for-clip";
      clipId: FrontendId;
    }
  | {
      type: "move-clip";
      clipId: FrontendId;
      direction: "up" | "down";
    }
  | {
      type: "add-chapter";
      name: string;
    }
  | {
      type: "update-chapter";
      chapterId: FrontendId;
      name: string;
    }
  | {
      type: "add-chapter-at";
      name: string;
      position: "before" | "after";
      itemId: FrontendId;
    }
  | {
      type: "chapter-created";
      frontendId: FrontendId;
      databaseId: DatabaseId;
    }
  | {
      type: "chapters-replaced";
      sections: Array<{
        databaseId: DatabaseId;
        name: string;
        beforeClipDatabaseId: DatabaseId;
      }>;
    }
  | {
      type: "restore-clip";
      clipId: FrontendId;
    }
  | {
      type: "permanently-remove-archived";
      sessionId: SessionId;
    }
  | {
      type: "permanently-remove-all-archived";
    }
  | {
      type: "add-effect-clip-at";
      effectType: "white-noise";
      position: "before" | "after";
      itemId: FrontendId;
    }
  | {
      type: "effect-clip-created";
      frontendId: FrontendId;
      databaseId: DatabaseId;
    }
  | {
      type: "effect-failed";
      effectType: string;
      message: string;
    }
  | {
      type: "update-clip-diagram-pin";
      clipId: FrontendId;
      diagramSnapshotId: string | null;
      diagramName: string | null;
    }
  | {
      type: "clip-audio-window-closed";
      sessionId: SessionId;
      activeDiagramId: string | null;
      diagramFocused: boolean;
      ts: number;
    }
  | {
      type: "set-clip-web-links";
      clipId: DatabaseId;
      webLinks: DB.ClipWebLink[];
    }
  | {
      type: "remove-clip-web-link";
      clipId: FrontendId;
      linkId: DatabaseId;
    };

export type ClipReducerEffect =
  | {
      type: "transcribe-clips";
      clipIds: DatabaseId[];
    }
  | {
      type: "archive-clips";
      clipIds: DatabaseId[];
    }
  /**
   * Scrolls the timeline to the insertion point. This is the editor's only
   * viewport jump, and only the recording flow fires it, so clips stay in view
   * as they land while you narrate. Every other action deliberately leaves the
   * timeline where it is (#1483) — don't dispatch this from one.
   */
  | {
      type: "scroll-to-insertion-point";
    }
  | {
      type: "update-clips";
      clips: [
        DatabaseId,
        { scene: string; profile: string; pauseType: PauseType },
      ][];
    }
  | {
      type: "update-pause";
      clipId: DatabaseId;
      pauseType: PauseType;
    }
  | {
      type: "update-zoom";
      clipId: DatabaseId;
      zoomType: ClipZoomType;
    }
  | {
      type: "reorder-clip";
      clipId: DatabaseId;
      direction: "up" | "down";
    }
  | {
      type: "reorder-chapter";
      chapterId: DatabaseId;
      direction: "up" | "down";
    }
  | {
      type: "archive-chapters";
      chapterIds: DatabaseId[];
    }
  | {
      type: "create-chapter";
      frontendId: FrontendId;
      name: string;
      insertionPoint: FrontendInsertionPoint;
    }
  | {
      type: "update-chapter";
      chapterId: DatabaseId;
      name: string;
    }
  | {
      type: "create-chapter-at";
      frontendId: FrontendId;
      name: string;
      position: "before" | "after";
      targetItemId: DatabaseId;
      targetItemType: "clip" | "chapter";
    }
  | {
      type: "create-effect-clip-at";
      frontendId: FrontendId;
      position: "before" | "after";
      targetItemId: DatabaseId;
      targetItemType: "clip" | "chapter";
      videoFilename: string;
      sourceStartTime: number;
      sourceEndTime: number;
      text: string;
      scene: string;
      profile: string;
      pauseType: string;
    }
  | {
      type: "start-session-timeout";
      sessionId: SessionId;
    }
  | {
      type: "unarchive-clips";
      clipIds: DatabaseId[];
    }
  | {
      type: "start-session-polling";
      sessionId: SessionId;
      outputPath: string;
      silenceLength: SilenceLength;
    }
  | {
      type: "revalidate-loader";
    }
  | {
      type: "snapshot-for-clip";
      diagramId: string;
      clipId: DatabaseId;
    }
  | {
      type: "persist-web-links";
      clipId: DatabaseId;
      links: CapturedWebLink[];
    }
  | {
      type: "delete-web-link";
      clipId: FrontendId;
      linkId: DatabaseId;
    };

export type ClipReducerExec = (effect: ClipReducerEffect) => void;
