import { db } from "@/db/client";
import { classics, episodes, mediaItems, seasons, storageFiles, storageTiers } from "@/db/schema";
import { cancelReplacement, requestReplacement } from "../replace-actions";
import { and, asc, desc, eq, isNull, sql } from "drizzle-orm";
import Link from "next/link";
import { notFound } from "next/navigation";
import { addTimelessClassic, removeTimelessClassic } from "../../actions";

export const dynamic = "force-dynamic";

type PageProps = {
  params: Promise<{ id: string }>;
};

function formatBytes(value: string | number | bigint | null) {
  const bytes = typeof value === "bigint" ? Number(value) : Number(value ?? 0);
  if (!Number.isFinite(bytes) || bytes <= 0) return "0 B";
  const units = ["B", "KB", "MB", "GB", "TB"];
  let size = bytes;
  let unit = 0;

  while (size >= 1024 && unit < units.length - 1) {
    size /= 1024;
    unit += 1;
  }

  return size.toFixed(unit === 0 ? 0 : 1) + " " + units[unit];
}

function formatDuration(seconds: number | null) {
  if (!seconds || seconds <= 0) return null;
  const hours = Math.floor(seconds / 3600);
  const minutes = Math.round((seconds % 3600) / 60);
  return hours ? `${hours}h ${minutes}m` : `${minutes}m`;
}

function seasonLabel(seasonNumber: number) {
  return seasonNumber === 0 ? "Specials" : "Season " + String(seasonNumber).padStart(2, "0");
}

const TIER_STYLES: Record<string, string> = {
  live: "border-admin-good text-admin-good",
  backup: "border-admin-line text-admin-muted",
  archive: "border-admin-accent text-admin-accent",
};

function TierBadge({ tier }: { tier: string | null }) {
  if (!tier) return null;
  return (
    <span className={"rounded-md border px-1.5 py-0.5 text-[11px] font-semibold uppercase tracking-wide " + (TIER_STYLES[tier] ?? "border-admin-line text-admin-muted")}>
      {tier}
    </span>
  );
}

export default async function AdminSeriesPage({ params }: PageProps) {
  const { id } = await params;

  const [item] = await db
    .select({
      id: mediaItems.id,
      title: mediaItems.title,
      year: mediaItems.year,
      overview: mediaItems.overview,
      isClassic: sql<boolean>`${classics.id} is not null`,
      classicNote: classics.note,
    })
    .from(mediaItems)
    .leftJoin(classics, eq(classics.mediaItemId, mediaItems.id))
    .where(and(eq(mediaItems.id, id), eq(mediaItems.mediaType, "tv_series")))
    .limit(1);

  if (!item) notFound();

  // Left join the files so an episode with no copy on any tier still renders.
  const rows = await db
    .select({
      seasonId: seasons.id,
      seasonNumber: seasons.seasonNumber,
      episodeId: episodes.id,
      episodeNumber: episodes.episodeNumber,
      episodeTitle: episodes.title,
      fileId: storageFiles.id,
      relativePath: storageFiles.relativePath,
      sizeBytes: storageFiles.sizeBytes,
      quality: storageFiles.quality,
      replaceRequestedAt: storageFiles.replaceRequestedAt,
      codec: storageFiles.codec,
      width: storageFiles.width,
      height: storageFiles.height,
      edition: storageFiles.edition,
      audioCodec: storageFiles.audioCodec,
      audioChannels: storageFiles.audioChannels,
      durationSeconds: storageFiles.durationSeconds,
      probedAt: storageFiles.probedAt,
      tier: storageTiers.tier,
    })
    .from(seasons)
    .innerJoin(episodes, eq(episodes.seasonId, seasons.id))
    .leftJoin(storageFiles, and(eq(storageFiles.episodeId, episodes.id), isNull(storageFiles.missingAt)))
    .leftJoin(storageTiers, eq(storageTiers.id, storageFiles.tierId))
    .where(eq(seasons.seriesId, item.id))
    .orderBy(desc(seasons.seasonNumber), asc(episodes.episodeNumber), asc(storageFiles.relativePath));

  // Backup mirrors live, so a backup row is byte-identical to a live one at the
  // same path. Listing it doubled every episode — and because backup is not
  // probed, its quality/codec are filename guesses that disagreed with live's
  // measured values on files that are the same bytes. Report it as coverage.
  const backupRows = await db
    .select({ relativePath: storageFiles.relativePath, sizeBytes: storageFiles.sizeBytes })
    .from(storageFiles)
    .innerJoin(storageTiers, eq(storageTiers.id, storageFiles.tierId))
    .where(
      and(
        eq(storageTiers.tier, "backup"),
        isNull(storageFiles.missingAt),
        eq(storageFiles.mediaItemId, item.id),
      ),
    );

  const backupIndex = new Map(backupRows.map((row) => [row.relativePath, String(row.sizeBytes ?? "")]));

  type Row = (typeof rows)[number];
  type Episode = { id: string; number: number; title: string | null; files: Row[] };
  type Season = { id: string; number: number; episodes: Episode[] };

  const seasonList: Season[] = [];
  const seasonIndex = new Map<string, Season>();
  const episodeIndex = new Map<string, Episode>();

  for (const row of rows) {
    let season = seasonIndex.get(row.seasonId);
    if (!season) {
      season = { id: row.seasonId, number: row.seasonNumber, episodes: [] };
      seasonIndex.set(row.seasonId, season);
      seasonList.push(season);
    }

    let episode = episodeIndex.get(row.episodeId);
    if (!episode) {
      episode = { id: row.episodeId, number: row.episodeNumber, title: row.episodeTitle, files: [] };
      episodeIndex.set(row.episodeId, episode);
      season.episodes.push(episode);
    }

    if (row.fileId && row.tier !== "backup") episode.files.push(row);
  }

  // "ok" mirrored at the same size · "stale" present at a different size ·
  // "missing" no backup copy · "n/a" archive, which is not mirrored to backup.
  function backupState(files: Row[]): "ok" | "stale" | "missing" | "n/a" {
    const live = files.filter((file) => file.tier === "live");
    if (!live.length) return "n/a";
    let worst: "ok" | "stale" | "missing" = "ok";
    for (const file of live) {
      const mirrored = backupIndex.get(file.relativePath ?? "");
      if (mirrored === undefined) return "missing";
      if (mirrored !== String(file.sizeBytes ?? "")) worst = "stale";
    }
    return worst;
  }

  // Newest season first, but Specials belong at the bottom rather than the top.
  seasonList.sort((a, b) => {
    if (a.number === 0) return 1;
    if (b.number === 0) return -1;
    return b.number - a.number;
  });

  const totalEpisodes = seasonList.reduce((sum, season) => sum + season.episodes.length, 0);
  const totalFiles = rows.filter((row) => row.fileId).length;
  const totalBytes = rows.reduce((sum, row) => sum + (row.fileId ? Number(row.sizeBytes ?? 0) : 0), 0);
  const missingEpisodes = seasonList.reduce(
    (sum, season) => sum + season.episodes.filter((episode) => !episode.files.length).length,
    0,
  );

  return (
    <div className="space-y-6">
      <div className="flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between">
        <div className="min-w-0">
          <Link href="/admin/inventory?kind=tv" className="text-xs font-semibold uppercase tracking-[0.32em] text-admin-accent">
            &larr; Television inventory
          </Link>
          <h1 className="mt-2 font-serif text-3xl font-semibold">{item.title}</h1>
          <div className="mt-2 flex flex-wrap items-center gap-2 text-xs">
            <span className="rounded-md border border-admin-line bg-admin-subpanel px-2 py-1 text-admin-muted">
              {seasonList.length} seasons
            </span>
            <span className="rounded-md border border-admin-line bg-admin-subpanel px-2 py-1 text-admin-muted">
              {totalEpisodes} episodes
            </span>
            <span className="rounded-md border border-admin-line bg-admin-subpanel px-2 py-1 text-admin-muted">
              {totalFiles} files / {formatBytes(totalBytes)}
            </span>
            {missingEpisodes ? (
              <span className="rounded-md border border-admin-warn bg-admin-subpanel px-2 py-1 font-semibold text-admin-warn">
                {missingEpisodes} without a file
              </span>
            ) : null}
            {item.isClassic ? (
              <span className="rounded-md border border-admin-accent bg-admin-subpanel px-2 py-1 font-semibold text-admin-accent">
                Timeless Classic
              </span>
            ) : null}
          </div>
          <p className="mt-3 max-w-4xl text-sm text-admin-muted">{item.overview ?? "No canonical overview stored yet."}</p>
        </div>

        <div className="w-full shrink-0 rounded-md border border-admin-line bg-admin-subpanel p-4 lg:w-72">
          {item.isClassic ? (
            <form action={removeTimelessClassic} className="space-y-3">
              <input type="hidden" name="mediaItemId" value={item.id} />
              <p className="text-sm text-admin-muted">Locked in main and blocked from Watch Now.</p>
              {item.classicNote ? <p className="text-sm">{item.classicNote}</p> : null}
              <button type="submit" className="admin-nav-button w-full px-3 py-2 text-sm font-medium">Remove Classic Lock</button>
            </form>
          ) : (
            <form action={addTimelessClassic} className="space-y-3">
              <label className="block text-sm font-medium" htmlFor="series-note">Timeless note</label>
              <input type="hidden" name="mediaItemId" value={item.id} />
              <textarea id="series-note" name="note" rows={3} className="w-full rounded-md border border-admin-line bg-admin-subpanel p-2 text-sm text-admin-text outline-none focus:border-admin-accent" placeholder="Why this stays on main forever" />
              <button type="submit" className="admin-nav-button w-full px-3 py-2 text-sm font-medium">Mark Timeless Classic</button>
            </form>
          )}
        </div>
      </div>

      <div className="space-y-3">
        {seasonList.map((season, index) => {
          const seasonFiles = season.episodes.flatMap((episode) => episode.files);
          const seasonBytes = seasonFiles.reduce((sum, file) => sum + Number(file.sizeBytes ?? 0), 0);
          const seasonMissing = season.episodes.filter((episode) => !episode.files.length).length;
          const tiers = Array.from(new Set(seasonFiles.map((file) => file.tier).filter(Boolean))) as string[];

          return (
            <details key={season.id} open={index === 0} className="admin-panel overflow-hidden">
              <summary className="flex cursor-pointer flex-wrap items-center justify-between gap-3 px-5 py-4 hover:bg-admin-subpanel">
                <div className="flex flex-wrap items-center gap-2">
                  <h2 className="font-serif text-xl font-semibold">{seasonLabel(season.number)}</h2>
                  {tiers.map((tier) => <TierBadge key={tier} tier={tier} />)}
                  {seasonMissing ? (
                    <span className="rounded-md border border-admin-warn px-1.5 py-0.5 text-[11px] font-semibold uppercase tracking-wide text-admin-warn">
                      {seasonMissing} missing
                    </span>
                  ) : null}
                </div>
                <p className="text-xs text-admin-muted">
                  {season.episodes.length} episodes &middot; {seasonFiles.length} files &middot; {formatBytes(seasonBytes)}
                </p>
              </summary>

              <div className="divide-y divide-admin-line border-t border-admin-line">
                {season.episodes.map((episode) => {
                  const hasFile = episode.files.length > 0;
                  return (
                    <div
                      key={episode.id}
                      className={hasFile ? "px-5 py-3" : "bg-admin-missing px-5 py-3"}
                    >
                      <div className="flex flex-wrap items-baseline gap-x-3 gap-y-1">
                        <span className="font-mono text-sm text-admin-muted">
                          {"E" + String(episode.number).padStart(2, "0")}
                        </span>
                        <span className="text-sm font-medium">{episode.title ?? "Untitled episode"}</span>
                        {hasFile ? (
                          (() => {
                            const state = backupState(episode.files);
                            if (state === "n/a") return null;
                            if (state === "ok") {
                              return <span title="Mirrored to backup at the same size" className="text-xs font-semibold text-admin-good">✓ backed up</span>;
                            }
                            if (state === "stale") {
                              return <span title="A backup copy exists at this path but its size differs from live" className="text-xs font-semibold text-admin-warn">! backup differs</span>;
                            }
                            return <span title="No backup copy of this file" className="text-xs font-semibold text-admin-warn">✗ not backed up</span>;
                          })()
                        ) : (
                          <span className="text-xs font-semibold uppercase tracking-wide text-admin-muted">Not in collection</span>
                        )}
                      </div>

                      {episode.files.map((file) => {
                        const resolution = file.width && file.height ? `${file.width}x${file.height}` : null;
                        const duration = formatDuration(file.durationSeconds);
                        const audio = file.audioCodec
                          ? file.audioCodec + (file.audioChannels ? ` ${file.audioChannels}ch` : "")
                          : null;
                        return (
                          <div key={file.fileId} className="mt-2 rounded-md border border-admin-line bg-admin-subpanel px-3 py-2">
                            <div className="flex flex-wrap items-center gap-2 text-xs">
                              <TierBadge tier={file.tier} />
                              {file.edition ? (
                                <span className="rounded-md border border-admin-accent px-1.5 py-0.5 font-semibold uppercase text-admin-accent">
                                  {file.edition === "bw" ? "B&W" : file.edition}
                                </span>
                              ) : null}
                              <span className="font-semibold">{file.quality ?? "Unknown"}</span>
                              {resolution ? <span className="text-admin-muted">{resolution}</span> : null}
                              <span className="text-admin-muted">{file.codec ?? "Unknown"}</span>
                              {audio ? <span className="text-admin-muted">{audio}</span> : null}
                              {duration ? <span className="text-admin-muted">{duration}</span> : null}
                              <span className="text-admin-muted">{formatBytes(file.sizeBytes)}</span>
                              {file.probedAt ? null : (
                                <span className="text-admin-muted italic">from filename, not probed</span>
                              )}
                            </div>
                            <p className="mt-1 break-all text-xs text-admin-muted">{file.relativePath}</p>
                            {/* Live only. A request is about the copy being
                                served, and flagging a backup or archive file
                                would mark something the reaper never looks at
                                and so do nothing at all. */}
                            {file.tier === "live" ? (
                              <div className="mt-2 flex items-center gap-2">
                                {file.replaceRequestedAt ? (
                                  <>
                                    <span className="text-xs font-semibold text-admin-warn">
                                      Replacement wanted — at the front of the queue
                                    </span>
                                    <form action={cancelReplacement}>
                                      <input type="hidden" name="fileId" value={file.fileId ?? ""} />
                                      <input type="hidden" name="seriesId" value={id} />
                                      <button
                                        type="submit"
                                        className="rounded border border-admin-line px-2 py-0.5 text-xs text-admin-muted transition hover:text-admin-text"
                                      >
                                        Cancel
                                      </button>
                                    </form>
                                  </>
                                ) : (
                                  <form action={requestReplacement}>
                                    <input type="hidden" name="fileId" value={file.fileId ?? ""} />
                                    <input type="hidden" name="seriesId" value={id} />
                                    <button
                                      type="submit"
                                      title="Look for a better copy, and delete this one once it arrives"
                                      className="rounded border border-admin-line px-2 py-0.5 text-xs text-admin-muted transition hover:border-admin-accent hover:text-admin-accent"
                                    >
                                      Replace this copy
                                    </button>
                                  </form>
                                )}
                              </div>
                            ) : null}
                          </div>
                        );
                      })}
                    </div>
                  );
                })}
              </div>
            </details>
          );
        })}
      </div>
    </div>
  );
}
