/**
 * <folk-splat-viewer> — Gaussian splat gallery + 3D viewer web component.
 *
 * Gallery mode: card grid of splats with upload form (splat files or photos/video).
 * Viewer mode: full-viewport Three.js + GaussianSplats3D renderer.
 *
 * Three.js and GaussianSplats3D are loaded via CDN importmap (not bundled).
 */

import { splatScenesSchema, splatScenesDocId, type SplatScenesDoc } from "../schemas";
import type { DocumentId } from "../../../shared/local-first/document";

interface SplatItem {
  id: string;
  slug: string;
  title: string;
  description?: string;
  file_format: string;
  file_size_bytes: number;
  view_count: number;
  contributor_name?: string;
  processing_status?: string;
  source_file_count?: number;
  created_at: string;
}

export class FolkSplatViewer extends HTMLElement {
  private _mode: "gallery" | "viewer" = "gallery";
  private _splats: SplatItem[] = [];
  private _spaceSlug = "demo";
  private _splatUrl = "";
  private _splatTitle = "";
  private _splatDesc = "";
  private _viewer: any = null;
  private _uploadMode: "splat" | "media" = "splat";
  private _offlineUnsub: (() => void) | null = null;

  static get observedAttributes() {
    return ["mode", "splat-url", "splat-title", "splat-desc", "space-slug"];
  }

  set splats(val: SplatItem[]) {
    this._splats = val;
    if (this._mode === "gallery") this.renderGallery();
  }

  set spaceSlug(val: string) {
    this._spaceSlug = val;
  }

  connectedCallback() {
    this._mode = (this.getAttribute("mode") as "gallery" | "viewer") || "gallery";
    this._splatUrl = this.getAttribute("splat-url") || "";
    this._splatTitle = this.getAttribute("splat-title") || "";
    this._splatDesc = this.getAttribute("splat-desc") || "";
    this._spaceSlug = this.getAttribute("space-slug") || "demo";

    if (this._mode === "viewer") {
      this.renderViewer();
    } else {
      if (this._spaceSlug === "demo") {
        this.loadDemoData();
      } else {
        this.subscribeOffline();
      }
      this.renderGallery();
    }
  }

  private async subscribeOffline() {
    const runtime = (window as any).__rspaceOfflineRuntime;
    if (!runtime?.isInitialized) return;

    try {
      const docId = splatScenesDocId(this._spaceSlug) as DocumentId;
      const doc = await runtime.subscribe(docId, splatScenesSchema);
      this.renderFromDoc(doc);

      this._offlineUnsub = runtime.onChange(docId, (updated: any) => {
        this.renderFromDoc(updated);
      });
    } catch {
      // Runtime unavailable — server-side hydration handles data
    }
  }

  private renderFromDoc(doc: SplatScenesDoc) {
    if (!doc?.items || Object.keys(doc.items).length === 0) return;
    if (this._splats.length > 0) return; // Don't clobber server-hydrated data
    this._splats = Object.values(doc.items).map(s => ({
      id: s.id, slug: s.slug, title: s.title, description: s.description,
      file_format: s.fileFormat, file_size_bytes: s.fileSizeBytes,
      view_count: s.viewCount, contributor_name: s.contributorName ?? undefined,
      processing_status: s.processingStatus ?? undefined,
      source_file_count: s.sourceFileCount,
      created_at: new Date(s.createdAt).toISOString(),
    }));
    if (this._mode === "gallery") this.renderGallery();
  }

  private loadDemoData() {
    this._splats = [
      { id: "s1", slug: "matterhorn-scan", title: "Matterhorn Summit", description: "Photogrammetry capture of the Matterhorn peak from drone footage, 42 source images.", file_format: "splat", file_size_bytes: 18_874_368, view_count: 284, contributor_name: "Alpine Explorer Team", processing_status: "ready", created_at: "2026-02-10" },
      { id: "s2", slug: "community-garden", title: "Community Garden Plot", description: "3D scan of the shared garden space — beds, paths, tool shed.", file_format: "ply", file_size_bytes: 24_117_248, view_count: 156, contributor_name: "Garden Collective", processing_status: "ready", created_at: "2026-02-15" },
      { id: "s3", slug: "print-shop-interior", title: "Print Shop Interior", description: "Interior scan of Druckwerkstatt Berlin — letterpress, risograph, binding station.", file_format: "spz", file_size_bytes: 31_457_280, view_count: 93, contributor_name: "Druckwerkstatt", processing_status: "ready", created_at: "2026-02-18" },
      { id: "s4", slug: "chamonix-trailhead", title: "Chamonix Trailhead", description: "360° capture of the Lac Blanc trailhead parking area and signage.", file_format: "splat", file_size_bytes: 12_582_912, view_count: 67, processing_status: "ready", created_at: "2026-02-20" },
      { id: "s5", slug: "zermatt-bridge", title: "Zermatt Suspension Bridge", description: "Charles Kuonen bridge scan from 18 photos. Processing complete.", file_format: "ply", file_size_bytes: 0, view_count: 0, source_file_count: 18, processing_status: "processing", created_at: "2026-02-25" },
      { id: "s6", slug: "mycorrhiza-sculpture", title: "Mycorrhiza Sculpture", description: "Uploaded for 3D reconstruction from video. Queued.", file_format: "splat", file_size_bytes: 0, view_count: 0, source_file_count: 1, processing_status: "pending", created_at: "2026-02-27" },
    ];
  }

  disconnectedCallback() {
    this._offlineUnsub?.();
    this._offlineUnsub = null;
    if (this._viewer) {
      try { this._viewer.dispose(); } catch {}
      this._viewer = null;
    }
  }

  attributeChangedCallback(name: string, _old: string, val: string) {
    if (name === "mode") this._mode = val as "gallery" | "viewer";
    if (name === "splat-url") this._splatUrl = val;
    if (name === "splat-title") this._splatTitle = val;
    if (name === "splat-desc") this._splatDesc = val;
    if (name === "space-slug") this._spaceSlug = val;
  }

  // ── Gallery ──

  private renderGallery() {
    const cards = this._splats.map((s) => {
      const status = s.processing_status || "ready";
      const isReady = status === "ready";
      const tag = isReady ? "a" : "div";
      const href = isReady ? ` href="/${this._spaceSlug}/rsplat/view/${s.slug}"` : "";
      const statusClass = !isReady ? ` splat-card--${status}` : "";

      let overlay = "";
      if (status === "pending") {
        overlay = `<div class="splat-card__overlay"><span class="splat-badge splat-badge--pending">Queued</span></div>`;
      } else if (status === "processing") {
        overlay = `<div class="splat-card__overlay"><div class="splat-card__spinner"></div><span class="splat-badge splat-badge--processing">Generating...</span></div>`;
      } else if (status === "failed") {
        overlay = `<div class="splat-card__overlay"><span class="splat-badge splat-badge--failed">Failed</span></div>`;
      }

      const sourceInfo = !isReady && s.source_file_count
        ? `<span>${s.source_file_count} source file${s.source_file_count > 1 ? "s" : ""}</span>`
        : `<span>${s.view_count} views</span>`;

      return `
      <${tag} class="splat-card${statusClass}" data-collab-id="splat:${s.id}"${href}>
        <div class="splat-card__preview">
          ${overlay}
          <span>${isReady ? "🔮" : "📸"}</span>
        </div>
        <div class="splat-card__body">
          <div class="splat-card__title">${esc(s.title)}</div>
          <div class="splat-card__meta">
            <span class="splat-badge splat-badge--${s.file_format}">${s.file_format}</span>
            ${isReady ? `<span>${formatSize(s.file_size_bytes)}</span>` : ""}
            ${sourceInfo}
          </div>
        </div>
      </${tag}>
    `;
    }).join("");

    const empty = this._splats.length === 0 ? `
      <div class="splat-empty">
        <div class="splat-empty__icon">🔮</div>
        <h3>No splats yet</h3>
        <p>Upload a .ply, .splat, or .spz file — or photos/video to generate one</p>
      </div>
    ` : "";

    this.innerHTML = `
      <div class="splat-gallery">
        ${empty}
        <div class="splat-grid">${cards}</div>
        <div class="splat-upload" id="splat-drop">
          <div class="splat-upload__toggle">
            <button class="splat-upload__toggle-btn active" data-mode="splat">Upload Splat</button>
            <button class="splat-upload__toggle-btn" data-mode="media">Upload Photos/Video</button>
          </div>

          <!-- Splat upload mode -->
          <div class="splat-upload__mode" id="splat-mode-splat">
            <div class="splat-upload__icon">📤</div>
            <p class="splat-upload__text">
              Drag &amp; drop a <strong>.ply</strong>, <strong>.splat</strong>, or <strong>.spz</strong> file here
              <br>or <strong id="splat-browse">browse</strong> to upload
            </p>
            <input type="file" id="splat-file" accept=".ply,.splat,.spz" hidden>
            <div class="splat-upload__form" id="splat-form">
              <input type="text" id="splat-title-input" placeholder="Title (required)" required>
              <textarea id="splat-desc-input" placeholder="Description (optional)" rows="2"></textarea>
              <input type="text" id="splat-tags-input" placeholder="Tags (comma-separated)">
              <button class="splat-upload__btn" id="splat-submit" disabled>Upload Splat</button>
              <div class="splat-upload__status" id="splat-status"></div>
            </div>
          </div>

          <!-- Media upload mode -->
          <div class="splat-upload__mode" id="splat-mode-media" style="display:none">
            <div class="splat-upload__icon">📸</div>
            <p class="splat-upload__text">
              Upload <strong>photos</strong> (up to 100) or a <strong>video</strong> to generate a 3D splat
              <br>Accepted: .jpg, .png, .heic, .mp4, .mov, .webm
              <br>or <strong id="media-browse">browse</strong> to select files
            </p>
            <input type="file" id="media-files" accept=".jpg,.jpeg,.png,.heic,.mp4,.mov,.webm" multiple hidden>
            <div class="splat-upload__selected" id="media-selected"></div>
            <div class="splat-upload__form" id="media-form">
              <input type="text" id="media-title-input" placeholder="Title (required)" required>
              <textarea id="media-desc-input" placeholder="Description (optional)" rows="2"></textarea>
              <input type="text" id="media-tags-input" placeholder="Tags (comma-separated)">
              <button class="splat-upload__btn" id="media-submit" disabled>Upload for Processing</button>
              <div class="splat-upload__status" id="media-status"></div>
            </div>
          </div>
        </div>
      </div>
    `;

    this.setupUploadHandlers();
    this.setupMediaHandlers();
    this.setupToggle();
  }

  private setupToggle() {
    const buttons = this.querySelectorAll<HTMLButtonElement>(".splat-upload__toggle-btn");
    const splatMode = this.querySelector("#splat-mode-splat") as HTMLElement;
    const mediaMode = this.querySelector("#splat-mode-media") as HTMLElement;

    buttons.forEach((btn) => {
      btn.addEventListener("click", () => {
        const mode = btn.dataset.mode as "splat" | "media";
        this._uploadMode = mode;
        buttons.forEach((b) => b.classList.toggle("active", b.dataset.mode === mode));
        splatMode.style.display = mode === "splat" ? "" : "none";
        mediaMode.style.display = mode === "media" ? "" : "none";
      });
    });
  }

  private setupUploadHandlers() {
    const drop = this.querySelector("#splat-drop") as HTMLElement;
    const fileInput = this.querySelector("#splat-file") as HTMLInputElement;
    const browse = this.querySelector("#splat-browse") as HTMLElement;
    const form = this.querySelector("#splat-form") as HTMLElement;
    const titleInput = this.querySelector("#splat-title-input") as HTMLInputElement;
    const descInput = this.querySelector("#splat-desc-input") as HTMLTextAreaElement;
    const tagsInput = this.querySelector("#splat-tags-input") as HTMLInputElement;
    const submitBtn = this.querySelector("#splat-submit") as HTMLButtonElement;
    const status = this.querySelector("#splat-status") as HTMLElement;

    if (!drop || !fileInput) return;

    let selectedFile: File | null = null;

    browse?.addEventListener("click", () => fileInput.click());

    fileInput.addEventListener("change", () => {
      if (fileInput.files?.[0]) {
        selectedFile = fileInput.files[0];
        form.classList.add("active");
        const name = selectedFile.name.replace(/\.(ply|splat|spz)$/i, "");
        titleInput.value = name.replace(/[-_]/g, " ");
        titleInput.dispatchEvent(new Event("input"));
      }
    });

    drop.addEventListener("dragover", (e) => {
      e.preventDefault();
      drop.classList.add("splat-upload--dragover");
    });

    drop.addEventListener("dragleave", () => {
      drop.classList.remove("splat-upload--dragover");
    });

    drop.addEventListener("drop", (e) => {
      e.preventDefault();
      drop.classList.remove("splat-upload--dragover");
      const file = e.dataTransfer?.files[0];
      if (this._uploadMode === "splat" && file && /\.(ply|splat|spz)$/i.test(file.name)) {
        selectedFile = file;
        form.classList.add("active");
        const name = file.name.replace(/\.(ply|splat|spz)$/i, "");
        titleInput.value = name.replace(/[-_]/g, " ");
        titleInput.dispatchEvent(new Event("input"));
      }
    });

    titleInput?.addEventListener("input", () => {
      submitBtn.disabled = !titleInput.value.trim() || !selectedFile;
    });

    submitBtn?.addEventListener("click", async () => {
      if (!selectedFile || !titleInput.value.trim()) return;

      submitBtn.disabled = true;
      status.textContent = "Uploading...";

      const formData = new FormData();
      formData.append("file", selectedFile);
      formData.append("title", titleInput.value.trim());
      formData.append("description", descInput.value.trim());
      formData.append("tags", tagsInput.value.trim());

      try {
        const token = localStorage.getItem("encryptid_token") || "";
        const res = await fetch(`/${this._spaceSlug}/rsplat/api/splats`, {
          method: "POST",
          headers: token ? { Authorization: `Bearer ${token}` } : {},
          body: formData,
        });

        if (res.status === 402) {
          status.textContent = "Payment required for upload (x402)";
          submitBtn.disabled = false;
          return;
        }

        if (res.status === 401) {
          status.textContent = "Sign in with rStack Identity to upload";
          submitBtn.disabled = false;
          return;
        }

        if (!res.ok) {
          const err = await res.json().catch(() => ({ error: "Upload failed" }));
          status.textContent = (err as any).error || "Upload failed";
          submitBtn.disabled = false;
          return;
        }

        const splat = await res.json() as SplatItem;
        status.textContent = "Uploaded!";
        setTimeout(() => {
          window.location.href = `/${this._spaceSlug}/rsplat/view/${splat.slug}`;
        }, 500);
      } catch (e) {
        status.textContent = "Network error";
        submitBtn.disabled = false;
      }
    });
  }

  private setupMediaHandlers() {
    const browse = this.querySelector("#media-browse") as HTMLElement;
    const fileInput = this.querySelector("#media-files") as HTMLInputElement;
    const form = this.querySelector("#media-form") as HTMLElement;
    const selected = this.querySelector("#media-selected") as HTMLElement;
    const titleInput = this.querySelector("#media-title-input") as HTMLInputElement;
    const descInput = this.querySelector("#media-desc-input") as HTMLTextAreaElement;
    const tagsInput = this.querySelector("#media-tags-input") as HTMLInputElement;
    const submitBtn = this.querySelector("#media-submit") as HTMLButtonElement;
    const status = this.querySelector("#media-status") as HTMLElement;

    if (!fileInput) return;

    let selectedFiles: File[] = [];

    browse?.addEventListener("click", () => fileInput.click());

    fileInput.addEventListener("change", () => {
      if (fileInput.files && fileInput.files.length > 0) {
        selectedFiles = Array.from(fileInput.files);
        form.classList.add("active");
        const totalSize = selectedFiles.reduce((sum, f) => sum + f.size, 0);
        selected.innerHTML = `<div class="splat-upload__file-count">${selectedFiles.length} file${selectedFiles.length > 1 ? "s" : ""} selected (${formatSize(totalSize)})</div>`;
        if (!titleInput.value.trim() && selectedFiles.length > 0) {
          const name = selectedFiles[0].name.replace(/\.[^.]+$/, "");
          titleInput.value = name.replace(/[-_]/g, " ");
        }
        titleInput.dispatchEvent(new Event("input"));
      }
    });

    titleInput?.addEventListener("input", () => {
      submitBtn.disabled = !titleInput.value.trim() || selectedFiles.length === 0;
    });

    submitBtn?.addEventListener("click", async () => {
      if (selectedFiles.length === 0 || !titleInput.value.trim()) return;

      submitBtn.disabled = true;
      status.textContent = "Uploading...";

      const formData = new FormData();
      for (const f of selectedFiles) {
        formData.append("files", f);
      }
      formData.append("title", titleInput.value.trim());
      formData.append("description", descInput.value.trim());
      formData.append("tags", tagsInput.value.trim());

      try {
        const token = localStorage.getItem("encryptid_token") || "";
        const res = await fetch(`/${this._spaceSlug}/rsplat/api/splats/from-media`, {
          method: "POST",
          headers: token ? { Authorization: `Bearer ${token}` } : {},
          body: formData,
        });

        if (res.status === 402) {
          status.textContent = "Payment required for upload (x402)";
          submitBtn.disabled = false;
          return;
        }

        if (res.status === 401) {
          status.textContent = "Sign in with rStack Identity to upload";
          submitBtn.disabled = false;
          return;
        }

        if (!res.ok) {
          const err = await res.json().catch(() => ({ error: "Upload failed" }));
          status.textContent = (err as any).error || "Upload failed";
          submitBtn.disabled = false;
          return;
        }

        status.textContent = "Uploaded! Queued for processing.";
        setTimeout(() => {
          window.location.href = `/${this._spaceSlug}/rsplat`;
        }, 1000);
      } catch (e) {
        status.textContent = "Network error";
        submitBtn.disabled = false;
      }
    });
  }

  // ── Viewer ──

  private renderViewer() {
    this.innerHTML = `
      <div class="splat-viewer">
        <div class="splat-loading" id="splat-loading">
          <div class="splat-loading__spinner"></div>
          <div class="splat-loading__text">Loading splat...</div>
        </div>
        <div class="splat-viewer__controls">
          <a class="splat-viewer__back" href="/${this._spaceSlug}/rsplat">← Gallery</a>
        </div>
        ${this._splatTitle ? `
          <div class="splat-viewer__info">
            <p class="splat-viewer__title">${esc(this._splatTitle)}</p>
            ${this._splatDesc ? `<p class="splat-viewer__desc">${esc(this._splatDesc)}</p>` : ""}
          </div>
        ` : ""}
        <div id="splat-container" class="splat-viewer__canvas"></div>
      </div>
    `;

    this.initThreeViewer();
  }

  private async initThreeViewer() {
    const container = this.querySelector("#splat-container") as HTMLElement;
    const loading = this.querySelector("#splat-loading") as HTMLElement;

    if (!container || !this._splatUrl) return;

    try {
      // Dynamic import from CDN (via importmap)
      const GaussianSplats3D = await import("@mkkellogg/gaussian-splats-3d");

      const viewer = new GaussianSplats3D.Viewer({
        cameraUp: [0, 1, 0],
        initialCameraPosition: [5, 3, 5],
        initialCameraLookAt: [0, 0, 0],
        rootElement: container,
        sharedMemoryForWorkers: false,
      });

      this._viewer = viewer;

      viewer.addSplatScene(this._splatUrl, {
        showLoadingUI: false,
        progressiveLoad: true,
      })
      .then(() => {
        viewer.start();
        if (loading) loading.classList.add("hidden");
      })
      .catch((e: Error) => {
        console.error("[rSplat] Scene load error:", e);
        if (loading) {
          const text = loading.querySelector(".splat-loading__text");
          if (text) text.textContent = `Error: ${e.message}`;
          const spinner = loading.querySelector(".splat-loading__spinner") as HTMLElement;
          if (spinner) spinner.style.display = "none";
        }
      });
    } catch (e) {
      console.error("[rSplat] Viewer init error:", e);
      if (loading) {
        const text = loading.querySelector(".splat-loading__text");
        if (text) text.textContent = `Error loading viewer: ${(e as Error).message}`;
        const spinner = loading.querySelector(".splat-loading__spinner") as HTMLElement;
        if (spinner) spinner.style.display = "none";
      }
    }
  }
}

// ── Helpers ──

function esc(s: string): string {
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
}

function formatSize(bytes: number): string {
  if (bytes < 1024) return `${bytes} B`;
  if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
  return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
}

customElements.define("folk-splat-viewer", FolkSplatViewer);
