"use client";

import { useCallback, useEffect, useRef, useState } from "react";
import { useRouter } from "next/navigation";
import { Plan, Privacy } from "@prisma/client";
import { filenameWithoutExtension } from "@/lib/constants";
import { getVideoAttributionText } from "@/lib/branding";
import { audioTagsToMetadata } from "@/lib/audio-tags";
import { resolveYouTubeTitle } from "@/lib/titles";
import { SONG_TITLE_MAX } from "@/lib/layout";
import type { ItemMetadata } from "@/lib/types";
import { DEFAULT_LAYOUT, type LayoutSettings } from "@/lib/layout";
import { DEFAULT_WATERMARK, type WatermarkSettings } from "@/lib/watermark";
import { CategorySelect } from "./CategorySelect";
import { LayoutStudio } from "./LayoutStudio";
import { PlaylistSelect } from "./PlaylistSelect";
import { PrivacyToggle } from "./PrivacyToggle";
import { ResolutionSelect } from "./ResolutionSelect";
import { QuotaErrorMessage, UpgradeProLink } from "./UpgradeProLink";
import { UpgradeProButton } from "./UpgradeProButton";
import { WatermarkPaywallModal } from "./WatermarkPaywallModal";
import { FREE_UNWATERMARKED_VIDEO_LIMIT } from "@/lib/watermark-policy";

const UPLOAD_CONCURRENCY = 4;

type AudioItem = {
  id: string;
  file: File;
  path: string | null;
  uploading: boolean;
  itemImagePath: string | null;
  itemImageName: string | null;
  itemImagePreview: string | null;
  metadata: ItemMetadata;
};

function defaultMetadata(title = ""): ItemMetadata {
  return {
    title,
    description: "",
    tags: "",
    privacy: "PUBLIC" as Privacy,
    categoryId: "10",
    resolution: "1280x720",
    notifySubscribers: true,
    madeForKids: false,
    embeddable: true,
    creativeCommons: false,
    includeWatermark: true,
    playlistId: null,
    imagePath: null,
    artist: null,
    songTitle: null,
    watermark: { ...DEFAULT_WATERMARK },
    layout: { ...DEFAULT_LAYOUT },
  };
}

export function UploadForm() {
  const router = useRouter();
  const uploadSessionRef = useRef<string>(crypto.randomUUID());
  const [imageFile, setImageFile] = useState<File | null>(null);
  const [imagePath, setImagePath] = useState<string | null>(null);
  const [imagePreviewUrl, setImagePreviewUrl] = useState<string | null>(null);
  const [imageUploading, setImageUploading] = useState(false);
  const [audioItems, setAudioItems] = useState<AudioItem[]>([]);
  const [submitting, setSubmitting] = useState(false);
  const [error, setError] = useState<string | null>(null);
  const [playlistId, setPlaylistId] = useState("");
  const [jobWatermark, setJobWatermark] = useState<WatermarkSettings>({ ...DEFAULT_WATERMARK });
  const [jobLayout, setJobLayout] = useState<LayoutSettings>({ ...DEFAULT_LAYOUT });
  const [logoPreviewUrl, setLogoPreviewUrl] = useState<string | null>(null);
  const [quota, setQuota] = useState<{
    remaining: number;
    limit: number;
    plan: Plan;
    maxBatchSize: number;
    resetsIn: string;
    videoCredits: number;
    extraCredits: number;
    totalAvailable: number;
    used: number;
    selfHosted?: boolean;
    customWatermark?: boolean;
    perItemImages?: boolean;
    artTrackLayouts?: boolean;
    subscriptionStatus?: "free" | "pro";
    createdVideoCount?: number;
    watermarkFreeRemaining?: number | null;
  } | null>(null);
  const [showWatermarkPaywall, setShowWatermarkPaywall] = useState(false);
  const [watermarkDefaultsApplied, setWatermarkDefaultsApplied] = useState(false);

  const isPro = Boolean(quota?.selfHosted || quota?.plan === "PREMIUM");
  const canPerItemImages = Boolean(quota?.perItemImages || isPro);
  const canCustomWatermark = Boolean(quota?.customWatermark || isPro);
  const canArtTrackLayouts = Boolean(quota?.artTrackLayouts || isPro);

  const loadQuota = useCallback(async () => {
    const res = await fetch("/api/quota");
    if (res.ok) {
      const data = await res.json();
      setQuota({
        remaining: data.remaining,
        limit: data.limit,
        plan: data.plan,
        maxBatchSize: data.maxBatchSize,
        resetsIn: data.resetsIn,
        videoCredits: data.videoCredits ?? data.extraCredits ?? 0,
        extraCredits: data.extraCredits ?? data.videoCredits ?? 0,
        totalAvailable: data.totalAvailable ?? data.remaining,
        used: data.used ?? 0,
        selfHosted: Boolean(data.selfHosted),
        customWatermark: Boolean(data.customWatermark),
        perItemImages: Boolean(data.perItemImages),
        artTrackLayouts: Boolean(data.artTrackLayouts),
        subscriptionStatus: data.subscriptionStatus === "pro" ? "pro" : "free",
        createdVideoCount: typeof data.createdVideoCount === "number" ? data.createdVideoCount : 0,
        watermarkFreeRemaining:
          data.watermarkFreeRemaining === null || data.watermarkFreeRemaining === undefined
            ? null
            : Number(data.watermarkFreeRemaining),
      });
    }
  }, []);

  useEffect(() => {
    loadQuota();
  }, [loadQuota]);

  // Align free-tier watermark default with remaining watermark-free slots (once).
  useEffect(() => {
    if (!quota || watermarkDefaultsApplied) return;
    if (quota.selfHosted || quota.plan === "PREMIUM") {
      setWatermarkDefaultsApplied(true);
      return;
    }
    const remaining = quota.watermarkFreeRemaining ?? 0;
    setJobWatermark({
      ...DEFAULT_WATERMARK,
      mode: remaining > 0 ? "none" : "default",
    });
    setWatermarkDefaultsApplied(true);
  }, [quota, watermarkDefaultsApplied]);

  useEffect(() => {
    return () => {
      if (imagePreviewUrl) URL.revokeObjectURL(imagePreviewUrl);
      if (logoPreviewUrl) URL.revokeObjectURL(logoPreviewUrl);
      audioItems.forEach((a) => {
        if (a.itemImagePreview) URL.revokeObjectURL(a.itemImagePreview);
      });
    };
    // eslint-disable-next-line react-hooks/exhaustive-deps -- cleanup on unmount only
  }, []);

  async function uploadFile(
    file: File,
    type: "image" | "audio" | "logo" | "font",
  ): Promise<{ path: string; audioTags?: Parameters<typeof audioTagsToMetadata>[0] | null }> {
    const formData = new FormData();
    formData.append("file", file);
    formData.append("type", type);
    formData.append("session", uploadSessionRef.current);
    const res = await fetch("/api/upload", { method: "POST", body: formData });
    const data = await res.json();
    if (!res.ok) {
      throw new Error(data.error || "Upload failed");
    }
    return { path: data.path, audioTags: data.audioTags };
  }

  async function uploadFilesWithConcurrency(files: File[]) {
    const pending = files.map((file) => {
      const id = crypto.randomUUID();
      const autoTitle = filenameWithoutExtension(file.name);

      setAudioItems((prev) => [
        ...prev,
        {
          id,
          file,
          path: null,
          uploading: true,
          itemImagePath: null,
          itemImageName: null,
          itemImagePreview: null,
          metadata: {
            ...defaultMetadata(autoTitle),
            playlistId: playlistId || null,
            watermark: { ...jobWatermark },
            layout: { ...jobLayout },
          },
        },
      ]);

      return { id, file, autoTitle };
    });

    for (let i = 0; i < pending.length; i += UPLOAD_CONCURRENCY) {
      const chunk = pending.slice(i, i + UPLOAD_CONCURRENCY);
      const results = await Promise.allSettled(
        chunk.map(async ({ id, file, autoTitle }) => {
          const upload = await uploadFile(file, "audio");
          const tagMetadata = upload.audioTags
            ? audioTagsToMetadata(upload.audioTags, autoTitle)
            : null;

          setAudioItems((prev) =>
            prev.map((item) =>
              item.id === id
                ? {
                    ...item,
                    path: upload.path,
                    uploading: false,
                    metadata: tagMetadata
                      ? { ...item.metadata, ...tagMetadata }
                      : item.metadata,
                  }
                : item,
            ),
          );
        }),
      );

      for (let j = 0; j < results.length; j++) {
        if (results[j].status === "rejected") {
          const { id } = chunk[j];
          const reason = results[j] as PromiseRejectedResult;
          setError(reason.reason instanceof Error ? reason.reason.message : "Audio upload failed");
          setAudioItems((prev) => prev.filter((item) => item.id !== id));
        }
      }
    }
  }

  async function handleImageChange(e: React.ChangeEvent<HTMLInputElement>) {
    const file = e.target.files?.[0];
    if (!file) return;
    setError(null);
    setImageFile(file);
    setImageUploading(true);
    if (imagePreviewUrl) URL.revokeObjectURL(imagePreviewUrl);
    setImagePreviewUrl(URL.createObjectURL(file));
    try {
      const path = await uploadFile(file, "image");
      setImagePath(path.path);
    } catch (err) {
      setError(err instanceof Error ? err.message : "Image upload failed");
      setImageFile(null);
      setImagePath(null);
      if (imagePreviewUrl) URL.revokeObjectURL(imagePreviewUrl);
      setImagePreviewUrl(null);
    } finally {
      setImageUploading(false);
    }
  }

  async function handleItemImageChange(itemId: string, file: File | null) {
    if (!canPerItemImages) {
      setError("Matching a unique image per audio requires Pro.");
      return;
    }
    if (!file) return;
    setError(null);
    const preview = URL.createObjectURL(file);
    setAudioItems((prev) =>
      prev.map((item) => {
        if (item.id !== itemId) return item;
        if (item.itemImagePreview) URL.revokeObjectURL(item.itemImagePreview);
        return {
          ...item,
          itemImageName: file.name,
          itemImagePreview: preview,
          itemImagePath: null,
        };
      }),
    );
    try {
      const uploaded = await uploadFile(file, "image");
      setAudioItems((prev) =>
        prev.map((item) =>
          item.id === itemId
            ? {
                ...item,
                itemImagePath: uploaded.path,
                metadata: { ...item.metadata, imagePath: uploaded.path },
              }
            : item,
        ),
      );
    } catch (err) {
      setError(err instanceof Error ? err.message : "Per-item image upload failed");
      setAudioItems((prev) =>
        prev.map((item) => {
          if (item.id !== itemId) return item;
          if (item.itemImagePreview) URL.revokeObjectURL(item.itemImagePreview);
          return {
            ...item,
            itemImagePath: null,
            itemImageName: null,
            itemImagePreview: null,
            metadata: { ...item.metadata, imagePath: null },
          };
        }),
      );
    }
  }

  async function handleLogoUpload(file: File) {
    const preview = URL.createObjectURL(file);
    if (logoPreviewUrl) URL.revokeObjectURL(logoPreviewUrl);
    setLogoPreviewUrl(preview);
    const uploaded = await uploadFile(file, "logo");
    return uploaded.path;
  }

  async function handleFontUpload(file: File) {
    const uploaded = await uploadFile(file, "font");
    return uploaded.path;
  }

  function applyWatermarkToAll(next: WatermarkSettings) {
    const normalized = { ...DEFAULT_WATERMARK, ...next };
    setJobWatermark(normalized);
    setAudioItems((prev) =>
      prev.map((item) => ({
        ...item,
        metadata: {
          ...item.metadata,
          includeWatermark: normalized.mode !== "none",
          watermark: { ...normalized },
        },
      })),
    );
  }

  function applyLayoutToAll(next: LayoutSettings) {
    const normalized = { ...DEFAULT_LAYOUT, ...next };
    setJobLayout(normalized);
    setAudioItems((prev) =>
      prev.map((item) => ({
        ...item,
        metadata: {
          ...item.metadata,
          layout: { ...normalized },
        },
      })),
    );
  }

  async function handleAudioChange(e: React.ChangeEvent<HTMLInputElement>) {
    const files = Array.from(e.target.files || []);
    if (!files.length) return;
    setError(null);

    const maxBatch = quota?.maxBatchSize ?? 3;
    if (audioItems.length + files.length > maxBatch) {
      setError(`Your plan allows up to ${maxBatch} audio files per batch.`);
      e.target.value = "";
      return;
    }

    await uploadFilesWithConcurrency(files);
    e.target.value = "";
  }

  function updateItemMetadata(id: string, updates: Partial<ItemMetadata>) {
    setAudioItems((prev) =>
      prev.map((item) =>
        item.id === id
          ? { ...item, metadata: { ...item.metadata, ...updates } }
          : item,
      ),
    );
  }

  function handlePlaylistChange(nextPlaylistId: string) {
    setPlaylistId(nextPlaylistId);
    setAudioItems((prev) =>
      prev.map((item) => ({
        ...item,
        metadata: {
          ...item.metadata,
          playlistId: nextPlaylistId || null,
        },
      })),
    );
  }

  function removeAudioItem(id: string) {
    setAudioItems((prev) => prev.filter((item) => item.id !== id));
  }

  const readyAudios = audioItems.filter((a) => a.path && !a.uploading);
  const canSubmit =
    imagePath &&
    !imageUploading &&
    readyAudios.length > 0 &&
    readyAudios.every((a) =>
      Boolean(
        resolveYouTubeTitle(
          {
            title: a.metadata.title,
            songTitle: a.metadata.songTitle,
            artist: a.metadata.artist,
          },
          isPro ? Plan.PREMIUM : Plan.FREE,
        ),
      ),
    ) &&
    !submitting;

  async function submitJob() {
    if (!canSubmit || !imagePath) return;

    setSubmitting(true);
    setError(null);

    try {
      const forcedWatermark =
        !isPro &&
        !quota?.selfHosted &&
        (quota?.createdVideoCount ?? 0) >= FREE_UNWATERMARKED_VIDEO_LIMIT;
      const effectiveWatermark: WatermarkSettings = forcedWatermark
        ? { ...DEFAULT_WATERMARK, mode: "default" }
        : canCustomWatermark
          ? jobWatermark
          : {
              mode: jobWatermark.mode === "none" ? "none" : "default",
              position: "bottom-right",
              offsetX: 20,
              offsetY: 20,
              text: null,
              logoPath: null,
              fontKey: "system",
              fontPath: null,
            };

      const res = await fetch("/api/jobs", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
          imagePath,
          items: readyAudios.map((item) => ({
            audioPath: item.path!,
            audioFilename: item.file.name,
            metadata: {
              ...item.metadata,
              imagePath: canPerItemImages ? item.itemImagePath || item.metadata.imagePath || null : null,
              includeWatermark: effectiveWatermark.mode !== "none",
              watermark: effectiveWatermark,
              layout: canArtTrackLayouts ? jobLayout : { ...DEFAULT_LAYOUT },
            },
          })),
        }),
      });

      const data = await res.json();
      if (!res.ok) {
        if (data.code === "PREMIUM_REQUIRED") {
          throw new Error(data.error || "This feature requires Pro.");
        }
        throw new Error(data.error || "Failed to create job");
      }

      router.push(`/jobs/${data.jobId}`);
    } catch (err) {
      setError(err instanceof Error ? err.message : "Submission failed");
      setSubmitting(false);
    }
  }

  async function handleSubmit(e: React.FormEvent) {
    e.preventDefault();
    if (!canSubmit || !imagePath) return;

    const needsPaywall =
      !isPro &&
      !quota?.selfHosted &&
      (quota?.createdVideoCount ?? 0) >= FREE_UNWATERMARKED_VIDEO_LIMIT;

    if (needsPaywall) {
      setShowWatermarkPaywall(true);
      return;
    }

    await submitJob();
  }

  function watermarkStatusLabel(): string | null {
    if (!quota || quota.selfHosted) return null;
    if (quota.plan === "PREMIUM" || quota.subscriptionStatus === "pro") {
      return "Pro Member (No Watermark)";
    }
    const remaining = quota.watermarkFreeRemaining ?? 0;
    if (remaining > 0) {
      return `Free Tier: ${remaining}/${FREE_UNWATERMARKED_VIDEO_LIMIT} Watermark-Free Renders Left`;
    }
    return "Free Tier (Watermark Active)";
  }

  return (
    <form onSubmit={handleSubmit} className="space-y-6">
      <WatermarkPaywallModal
        open={showWatermarkPaywall}
        onClose={() => setShowWatermarkPaywall(false)}
        onContinueWithWatermark={() => {
          setShowWatermarkPaywall(false);
          void submitJob();
        }}
      />

      {quota && (
        <div className="rounded border border-gray-700 bg-surface-light px-4 py-3 text-sm text-gray-300">
          {watermarkStatusLabel() ? (
            <p className="mb-2 font-medium text-white">{watermarkStatusLabel()}</p>
          ) : null}
          {quota.selfHosted ? (
            <>
              Self-hosted · {quota.used} videos processed · up to {quota.maxBatchSize} files per
              batch
            </>
          ) : (
            <>
              {quota.remaining} of {quota.limit} plan videos remaining
              {quota.extraCredits > 0
                ? ` · ${quota.extraCredits} extra credits (${quota.totalAvailable} total available)`
                : ""}{" "}
              ·{" "}
              {quota.plan === "FREE"
                ? `resets in ${quota.resetsIn}`
                : `monthly quota resets on ${quota.resetsIn}`}{" "}
              · up to {quota.maxBatchSize} files per batch
              {quota.plan === "FREE" ? (
                <>
                  {" · "}
                  <a href="/dashboard/settings" className="text-accent hover:underline">
                    Buy 1–15 credits
                  </a>
                </>
              ) : null}
            </>
          )}
        </div>
      )}

      {error && (
        <div className="rounded border border-red-500/50 bg-red-500/10 px-4 py-3 text-sm text-red-300">
          <QuotaErrorMessage message={error} />
        </div>
      )}

      <section className="rounded-lg border border-gray-700 bg-surface p-6 space-y-4">
        <h2 className="text-lg font-medium text-white">Files</h2>

        <div className="grid gap-4 md:grid-cols-2">
          <div>
            <label className="mb-2 block text-sm text-gray-400">Image</label>
            <input
              type="file"
              accept="image/*"
              onChange={handleImageChange}
              className="block w-full text-sm text-gray-300 file:mr-4 file:rounded file:border-0 file:bg-accent file:px-4 file:py-2 file:text-sm file:text-white"
            />
            <div className="mt-2 flex items-center gap-2 text-sm">
              <StatusDot ok={!!imagePath && !imageUploading} />
              <span className="text-gray-400">
                {imageUploading ? "Uploading…" : imageFile?.name || "No image selected"}
              </span>
            </div>
            <p className="mt-2 text-xs text-gray-500">
              {canPerItemImages
                ? "Shared cover for all tracks. Optionally override per audio below (Pro)."
                : "Free plan: one static cover for the whole batch."}
            </p>
          </div>

          <div>
            <label className="mb-2 block text-sm text-gray-400">Audio files</label>
            <input
              type="file"
              accept={
                quota?.selfHosted || quota?.plan === "PREMIUM"
                  ? "audio/mpeg,audio/wav,audio/flac,.mp3,.wav,.flac"
                  : "audio/mpeg,.mp3"
              }
              multiple
              onChange={handleAudioChange}
              className="block w-full text-sm text-gray-300 file:mr-4 file:rounded file:border-0 file:bg-accent file:px-4 file:py-2 file:text-sm file:text-white"
            />
            <div className="mt-2 flex items-center gap-2 text-sm">
              <StatusDot ok={readyAudios.length > 0} />
              <span className="text-gray-400">
                {audioItems.length === 0
                  ? "No audio files selected"
                  : `${readyAudios.length} of ${audioItems.length} ready`}
              </span>
            </div>
          </div>
        </div>
      </section>

      {audioItems.length > 0 && (
        <section className="space-y-4">
          <h2 className="text-lg font-medium text-white">Video details (per audio)</h2>
          <p className="text-sm text-gray-400">
            Each audio file gets its own metadata. Video title is used for YouTube; song title and
            artist appear in Pro art-track layouts.
          </p>

          <div className="rounded-lg border border-gray-700 bg-surface p-6">
            <PlaylistSelect
              value={playlistId}
              onChange={handlePlaylistChange}
              enabled={Boolean(quota?.selfHosted || quota?.plan === "PREMIUM")}
            />
          </div>

          {audioItems.map((item, index) => (
            <div
              key={item.id}
              className="rounded-lg border border-gray-700 bg-surface p-6 space-y-4"
            >
              <div className="flex items-center justify-between">
                <div>
                  <h3 className="font-medium text-white">
                    Video {index + 1}: {item.file.name}
                  </h3>
                  {item.uploading && (
                    <p className="text-xs text-yellow-400">Uploading…</p>
                  )}
                </div>
                <button
                  type="button"
                  onClick={() => removeAudioItem(item.id)}
                  className="text-sm text-red-400 hover:text-red-300"
                >
                  Remove
                </button>
              </div>

              <div className="grid gap-4 md:grid-cols-2">
                <Field label="Video title (YouTube)">
                  <input
                    type="text"
                    value={item.metadata.title}
                    onChange={(e) => updateItemMetadata(item.id, { title: e.target.value })}
                    className="input-field"
                    required
                    placeholder="Title shown on YouTube"
                  />
                </Field>

                <ProLockedField
                  label="Song title (on-video)"
                  locked={!isPro}
                  hint="Pro unlocks custom song title burned into art-track layouts."
                >
                  <input
                    type="text"
                    value={item.metadata.songTitle ?? ""}
                    onChange={(e) => updateItemMetadata(item.id, { songTitle: e.target.value })}
                    className="input-field disabled:cursor-not-allowed disabled:opacity-60"
                    disabled={!isPro}
                    placeholder={isPro ? "Shown in art-track layout" : "Pro feature"}
                    maxLength={SONG_TITLE_MAX}
                  />
                </ProLockedField>
              </div>

              <div className="grid gap-4 md:grid-cols-2">
                <ProLockedField
                  label="Artist (on-video)"
                  locked={!isPro}
                  hint="Pro unlocks custom artist line in art-track layouts."
                >
                  <input
                    type="text"
                    value={item.metadata.artist ?? ""}
                    onChange={(e) => updateItemMetadata(item.id, { artist: e.target.value })}
                    className="input-field disabled:cursor-not-allowed disabled:opacity-60"
                    disabled={!isPro}
                    placeholder={isPro ? "Shown in art-track layout" : "Pro feature"}
                    maxLength={80}
                  />
                </ProLockedField>
                <Field label="Category">
                  <CategorySelect
                    value={item.metadata.categoryId}
                    onChange={(v) => updateItemMetadata(item.id, { categoryId: v })}
                  />
                </Field>
              </div>

              <Field label="Description">
                <textarea
                  value={item.metadata.description}
                  onChange={(e) => updateItemMetadata(item.id, { description: e.target.value })}
                  rows={3}
                  className="input-field resize-y"
                />
              </Field>

              <Field label="Tags">
                <input
                  type="text"
                  value={item.metadata.tags}
                  onChange={(e) => updateItemMetadata(item.id, { tags: e.target.value })}
                  placeholder='Separate with spaces or commas. Use "quoted phrases" for multi-word tags.'
                  className="input-field"
                />
              </Field>

              <Field label="Privacy">
                <PrivacyToggle
                  value={item.metadata.privacy}
                  onChange={(v) => updateItemMetadata(item.id, { privacy: v })}
                />
              </Field>

              <Field label="Video size">
                <ResolutionSelect
                  value={item.metadata.resolution}
                  onChange={(v) => updateItemMetadata(item.id, { resolution: v })}
                  plan={quota?.plan}
                />
              </Field>

              <div className="relative rounded border border-gray-800 bg-surface-light/40 p-3">
                {!canPerItemImages && (
                  <div className="mb-2 flex items-center gap-2">
                    <span className="rounded border border-accent/40 bg-accent/10 px-1.5 py-0.5 text-[10px] font-semibold uppercase text-accent">
                      Pro
                    </span>
                    <span className="text-xs text-gray-500">Unique image per track</span>
                  </div>
                )}
                <Field
                  label={
                    canPerItemImages
                      ? "Cover for this track (optional)"
                      : "Cover for this track (Pro)"
                  }
                >
                  <input
                    type="file"
                    accept="image/*"
                    disabled={!canPerItemImages}
                    onChange={(e) => {
                      const f = e.target.files?.[0] ?? null;
                      void handleItemImageChange(item.id, f);
                      e.target.value = "";
                    }}
                    className="block w-full text-sm text-gray-300 file:mr-4 file:rounded file:border-0 file:bg-accent file:px-4 file:py-2 file:text-sm file:text-white disabled:opacity-50"
                  />
                </Field>
                {item.itemImageName && (
                  <p className="mt-1 text-xs text-gray-500">
                    {item.itemImagePath ? `Using ${item.itemImageName}` : `Uploading ${item.itemImageName}…`}
                  </p>
                )}
                {!canPerItemImages && (
                  <p className="mt-2 text-xs text-gray-500">
                    <UpgradeProButton label="Upgrade for bulk unique image matching" />
                  </p>
                )}
              </div>

              <div className="grid gap-3 sm:grid-cols-2">
                <Checkbox
                  label="Notify subscribers about this upload"
                  checked={item.metadata.notifySubscribers}
                  onChange={(v) => updateItemMetadata(item.id, { notifySubscribers: v })}
                />
                <Checkbox
                  label="Made For Kids?"
                  checked={item.metadata.madeForKids}
                  onChange={(v) => updateItemMetadata(item.id, { madeForKids: v })}
                />
                <Checkbox
                  label="Embeddable?"
                  checked={item.metadata.embeddable}
                  onChange={(v) => updateItemMetadata(item.id, { embeddable: v })}
                />
                <Checkbox
                  label="Creative Commons?"
                  checked={item.metadata.creativeCommons}
                  onChange={(v) => updateItemMetadata(item.id, { creativeCommons: v })}
                />
                {!canCustomWatermark && (
                  <Checkbox
                    label={
                      !isPro &&
                      (quota?.watermarkFreeRemaining ?? 0) <= 0 &&
                      !quota?.selfHosted
                        ? "Songs2VID watermark (required on Free after 2 clean renders)"
                        : `Show '${getVideoAttributionText()}' watermark (support open-source)`
                    }
                    checked={jobWatermark.mode !== "none"}
                    onChange={(v) => {
                      if (
                        !v &&
                        !isPro &&
                        !quota?.selfHosted &&
                        (quota?.watermarkFreeRemaining ?? 0) <= 0
                      ) {
                        return;
                      }
                      applyWatermarkToAll({
                        ...DEFAULT_WATERMARK,
                        mode: v ? "default" : "none",
                      });
                    }}
                  />
                )}
              </div>
            </div>
          ))}
        </section>
      )}

      <LayoutStudio
        locked={!canArtTrackLayouts && !canCustomWatermark}
        previewImageUrl={
          audioItems.find((a) => a.itemImagePreview)?.itemImagePreview || imagePreviewUrl
        }
        songTitle={
          audioItems[0]?.metadata.songTitle ||
          audioItems[0]?.metadata.title ||
          "Track title"
        }
        artist={audioItems[0]?.metadata.artist || ""}
        encodeWidth={parseInt(readyAudios[0]?.metadata.resolution?.split("x")[0] || "1280", 10)}
        layout={jobLayout}
        onLayoutChange={applyLayoutToAll}
        watermark={jobWatermark}
        onWatermarkChange={applyWatermarkToAll}
        onUploadLogo={handleLogoUpload}
        onUploadFont={handleFontUpload}
        logoPreviewUrl={logoPreviewUrl}
      />

      {quota && !quota.selfHosted && quota.plan === "FREE" && (
        <>
          <p className="text-sm text-gray-400">
            Free plan: first {FREE_UNWATERMARKED_VIDEO_LIMIT} successful videos are watermark-free;
            later renders include a Songs2VID watermark (bottom-right). One static cover image per
            batch.
          </p>
          <p className="text-sm text-gray-400">
            <UpgradeProLink className="text-accent hover:underline" /> for unwatermarked videos,
            custom branding, typography, blurred art-track layouts, unique image matching, 1080p, and
            lossless audio.
          </p>
        </>
      )}

      <button
        type="submit"
        disabled={!canSubmit}
        className="w-full rounded bg-accent px-6 py-3 font-medium text-white transition-colors hover:bg-accent-hover disabled:cursor-not-allowed disabled:opacity-50"
      >
        {submitting
          ? "Creating videos…"
          : `Create ${readyAudios.length || ""} Video${readyAudios.length !== 1 ? "s" : ""}`}
      </button>
    </form>
  );
}

function StatusDot({ ok }: { ok: boolean }) {
  return (
    <span
      className={`inline-block h-3 w-3 rounded-full ${ok ? "bg-green-500" : "bg-red-500"}`}
    />
  );
}

function Field({ label, children }: { label: string; children: React.ReactNode }) {
  return (
    <div>
      <label className="mb-1 block text-sm text-gray-400">{label}</label>
      {children}
    </div>
  );
}

function ProLockedField({
  label,
  locked,
  hint,
  children,
}: {
  label: string;
  locked: boolean;
  hint: string;
  children: React.ReactNode;
}) {
  return (
    <div>
      <label className="mb-1 flex items-center gap-2 text-sm text-gray-400">
        {label}
        {locked && (
          <span
            className="rounded border border-accent/50 bg-accent/15 px-1.5 py-0.5 text-[10px] font-semibold uppercase tracking-wider text-accent"
            title={hint}
          >
            Pro
          </span>
        )}
      </label>
      <div className="relative">
        {children}
        {locked && (
          <p className="mt-1 text-xs text-gray-500">{hint}</p>
        )}
      </div>
    </div>
  );
}

function Checkbox({
  label,
  checked,
  onChange,
  disabled,
}: {
  label: string;
  checked: boolean;
  onChange: (v: boolean) => void;
  disabled?: boolean;
}) {
  return (
    <label className={`flex items-center gap-2 text-sm text-gray-300 ${disabled ? "opacity-60" : ""}`}>
      <input
        type="checkbox"
        checked={checked}
        onChange={(e) => onChange(e.target.checked)}
        disabled={disabled}
        className="rounded border-gray-600 bg-surface-light text-accent focus:ring-accent"
      />
      {label}
    </label>
  );
}
