import {
  pgTable,
  uuid,
  text,
  boolean,
  timestamp,
  pgEnum,
  bigint,
  integer,
  index,
  uniqueIndex,
} from "drizzle-orm/pg-core";
import { episodes, mediaItems } from "./media";

export const storageTierEnum = pgEnum("storage_tier", [
  "live",
  "backup",
  "archive",
]);

export const storageTiers = pgTable("storage_tiers", {
  id: uuid("id").primaryKey().defaultRandom(),
  tier: storageTierEnum("tier").notNull().unique(),
  label: text("label").notNull(),
  basePath: text("base_path").notNull(),
  // Whether Ampelos should treat this tier as continuously available.
  // Live is always expected online; backup and archive may be offline.
  alwaysOnline: boolean("always_online").notNull().default(false),
  // Name of the agent (agent_heartbeats.agent) whose host owns this storage.
  // When set, reachability is derived from that agent's heartbeat freshness
  // rather than assumed. Null means availability is unknown/manual.
  agentName: text("agent_name"),
  notes: text("notes"),
});

// Tracks observed availability windows for tiers that are not always online.
export const storageAvailability = pgTable("storage_availability", {
  id: uuid("id").primaryKey().defaultRandom(),
  tierId: uuid("tier_id").notNull().references(() => storageTiers.id),
  online: boolean("online").notNull(),
  detectedAt: timestamp("detected_at").notNull().defaultNow(),
  // Null if the tier is still in this state.
  resolvedAt: timestamp("resolved_at"),
});


export const storageFiles = pgTable(
  "storage_files",
  {
    id: uuid("id").primaryKey().defaultRandom(),
    tierId: uuid("tier_id").notNull().references(() => storageTiers.id, { onDelete: "cascade" }),
    mediaItemId: uuid("media_item_id").references(() => mediaItems.id, { onDelete: "set null" }),
    episodeId: uuid("episode_id").references(() => episodes.id, { onDelete: "set null" }),
    relativePath: text("relative_path").notNull(),
    fullPath: text("full_path").notNull(),
    filename: text("filename").notNull(),
    extension: text("extension").notNull(),
    sizeBytes: bigint("size_bytes", { mode: "bigint" }).notNull(),
    modifiedAt: timestamp("modified_at").notNull(),
    observedAt: timestamp("observed_at").notNull().defaultNow(),
    missingAt: timestamp("missing_at"),
    inferredTitle: text("inferred_title"),
    inferredYear: text("inferred_year"),
    // Derived from the ffprobe columns below once a probe has run; until then
    // these hold the (unreliable) filename guess made by the scanners.
    quality: text("quality"),
    codec: text("codec"),
    // Alternate cut of the same episode/movie kept deliberately alongside the
    // standard one, e.g. the black-and-white release of Spider-Noir. Null means
    // the normal edition. Two files on one episode with different editions are
    // intentional and must not be treated as duplicates by dedup or policy.
    edition: text("edition"),
    // Ground truth read out of the container by ampelos-agent scripts/probe-media.mjs.
    // Filenames in this library are demonstrably wrong about both resolution
    // and codec, so anything user-facing should prefer these.
    width: integer("width"),
    height: integer("height"),
    videoCodec: text("video_codec"),
    audioCodec: text("audio_codec"),
    audioChannels: integer("audio_channels"),
    durationSeconds: integer("duration_seconds"),
    bitrate: bigint("bitrate", { mode: "bigint" }),
    probedAt: timestamp("probed_at"),
    // Last ffprobe failure for this file; cleared on a successful probe.
    probeError: text("probe_error"),
    // Somebody looked at this copy and asked for a better one.
    //
    // On the FILE rather than the episode, because that is what the request is
    // about: this copy, the one they were looking at, is the one to throw away
    // once something better has landed. An episode-level flag could not say
    // which of two copies was the offender.
    //
    // It does three things while it is set: the acquisition queue stops
    // treating the episode as already held, the queue puts it in the top band,
    // and the reaper is allowed to delete this file once a replacement is
    // actually on disk. Clearing it cancels all three.
    replaceRequestedAt: timestamp("replace_requested_at"),
    replaceRequestedBy: uuid("replace_requested_by"),
  },
  (t) => [
    uniqueIndex("storage_files_tier_relative_path_idx").on(t.tierId, t.relativePath),
    // "Which files does this title hold?" is the question almost every page
    // asks, and without this it was answered by scanning all 41,000 rows. The
    // link review queue asks it twice per title across 2,771 titles, which took
    // eighty seconds; the same scan was quietly taxing the inventory and title
    // pages too, just not enough for anyone to notice.
    index("storage_files_media_item_idx").on(t.mediaItemId),
  ],
);
