//! Embed podcast-spiller: GET /pub/{slug}/{episode_id}/player
//!
//! Serverer en selvstående HTML-side med podcast-spiller som kan embeddees
//! som iframe. Inkluderer artwork, tittel, play/pause, progress, waveform
//! (WaveSurfer.js) og kapittelmerkering.
//!
//! Ref: docs/concepts/publisering.md, docs/features/podcast_hosting.md

use axum::{
    extract::{Path, State},
    http::{header, StatusCode},
    response::Response,
};
use chrono::{DateTime, Utc};
use serde::Serialize;
use sqlx::PgPool;
use uuid::Uuid;

use crate::AppState;

// =============================================================================
// Data-modeller
// =============================================================================

#[derive(Serialize)]
struct EpisodePlayerData {
    title: String,
    podcast_title: String,
    audio_url: String,
    artwork_url: Option<String>,
    duration_secs: Option<i64>,
    chapters: Vec<ChapterData>,
    base_url: String,
}

#[derive(Serialize)]
struct ChapterData {
    at_secs: i64,
    at_display: String,
    title: String,
}

// =============================================================================
// Handler
// =============================================================================

/// GET /pub/{slug}/{episode_id}/player — embed-klar podcast-spiller.
pub async fn serve_player(
    State(state): State<AppState>,
    Path((slug, episode_id)): Path<(String, String)>,
) -> Result<Response, StatusCode> {
    let data = fetch_episode_player_data(&state.db, &slug, &episode_id)
        .await
        .map_err(|e| {
            tracing::error!(slug = %slug, episode = %episode_id, error = %e, "Feil ved henting av episode for spiller");
            StatusCode::INTERNAL_SERVER_ERROR
        })?
        .ok_or(StatusCode::NOT_FOUND)?;

    let html = render_player_html(&data);

    Ok(Response::builder()
        .header(header::CONTENT_TYPE, "text/html; charset=utf-8")
        .header(header::CACHE_CONTROL, "public, max-age=3600")
        // Tillat iframe-embedding fra alle domener
        .header("X-Frame-Options", "ALLOWALL")
        .header(
            "Content-Security-Policy",
            "frame-ancestors *; default-src 'self' 'unsafe-inline' https://unpkg.com; media-src 'self'; img-src 'self' data:",
        )
        .body(html.into())
        .unwrap())
}

// =============================================================================
// Database-spørring
// =============================================================================

async fn fetch_episode_player_data(
    db: &PgPool,
    slug: &str,
    episode_short_id: &str,
) -> Result<Option<EpisodePlayerData>, sqlx::Error> {
    // 1. Finn samling med publishing-trait
    let collection_row: Option<(Uuid, Option<String>, serde_json::Value)> = sqlx::query_as(
        r#"
        SELECT id, title, metadata
        FROM nodes
        WHERE node_kind = 'collection'
          AND metadata->'traits'->'publishing'->>'slug' = $1
        LIMIT 1
        "#,
    )
    .bind(slug)
    .fetch_optional(db)
    .await?;

    let Some((collection_id, collection_title, collection_metadata)) = collection_row else {
        return Ok(None);
    };

    let podcast_title = collection_title.unwrap_or_else(|| slug.to_string());

    // Base URL for CAS og lenker
    let base_url = collection_metadata
        .get("traits")
        .and_then(|t| t.get("publishing"))
        .and_then(|p| p.get("custom_domain"))
        .and_then(|d| d.as_str())
        .map(|d| format!("https://{d}"))
        .unwrap_or_else(|| format!("https://synops.no/pub/{slug}"));

    // 2. Finn episoden (belongs_to samlingen, short_id-match)
    let pattern = format!("{episode_short_id}%");
    let episode_row: Option<(Uuid, Option<String>, serde_json::Value, DateTime<Utc>, Option<serde_json::Value>)> =
        sqlx::query_as(
            r#"
            SELECT n.id, n.title, n.metadata, n.created_at, e.metadata
            FROM edges e
            JOIN nodes n ON n.id = e.source_id
            WHERE e.target_id = $1
              AND e.edge_type = 'belongs_to'
              AND n.id::text LIKE $2
            LIMIT 1
            "#,
        )
        .bind(collection_id)
        .bind(&pattern)
        .fetch_optional(db)
        .await?;

    let Some((episode_id, episode_title, _episode_metadata, created_at, edge_meta)) = episode_row
    else {
        return Ok(None);
    };

    // Sjekk publish_at — ikke server spilleren for upubliserte episoder
    let publish_at = edge_meta
        .as_ref()
        .and_then(|m| m.get("publish_at"))
        .and_then(|v| v.as_str())
        .and_then(|s| s.parse::<DateTime<Utc>>().ok())
        .unwrap_or(created_at);

    if publish_at > Utc::now() {
        return Ok(None);
    }

    // 3. Hent medienode (has_media-edge) for audio-URL og varighet
    let media_row: Option<(serde_json::Value,)> = sqlx::query_as(
        r#"
        SELECT m.metadata
        FROM edges e
        JOIN nodes m ON m.id = e.target_id
        WHERE e.source_id = $1
          AND e.edge_type = 'has_media'
          AND m.node_kind = 'media'
        LIMIT 1
        "#,
    )
    .bind(episode_id)
    .fetch_optional(db)
    .await?;

    let Some((media_metadata,)) = media_row else {
        return Ok(None); // Ingen lydfil — ingenting å spille
    };

    let cas_hash = media_metadata
        .get("cas_hash")
        .and_then(|v| v.as_str())
        .ok_or_else(|| {
            tracing::warn!(episode_id = %episode_id, "Media-node mangler cas_hash");
            sqlx::Error::RowNotFound
        })?;

    let audio_url = format!("{base_url}/cas/{cas_hash}");

    let duration_secs = media_metadata
        .get("duration_secs")
        .and_then(|v| v.as_i64());

    // 4. Hent artwork — episodens egen og fallback til samlingens
    let artwork_cas: Option<String> = sqlx::query_scalar(
        r#"
        SELECT m.metadata->>'cas_hash'
        FROM edges e
        JOIN nodes m ON m.id = e.source_id
        WHERE e.target_id = $1
          AND e.edge_type = 'og_image'
        LIMIT 1
        "#,
    )
    .bind(episode_id)
    .fetch_optional(db)
    .await?;

    // Fallback til samlingens artwork
    let artwork_cas = match artwork_cas {
        Some(h) => Some(h),
        None => {
            sqlx::query_scalar(
                r#"
                SELECT m.metadata->>'cas_hash'
                FROM edges e
                JOIN nodes m ON m.id = e.target_id
                WHERE e.source_id = $1
                  AND e.edge_type = 'og_image'
                LIMIT 1
                "#,
            )
            .bind(collection_id)
            .fetch_optional(db)
            .await?
        }
    };

    let artwork_url = artwork_cas.map(|h| format!("{base_url}/cas/{h}"));

    // 5. Hent presentasjonselementer (tittel-edge)
    let pres_title: Option<(Option<String>, Option<String>)> = sqlx::query_as(
        r#"
        SELECT n.title, n.content
        FROM edges e
        JOIN nodes n ON n.id = e.source_id
        WHERE e.target_id = $1
          AND e.edge_type = 'title'
        ORDER BY e.created_at DESC
        LIMIT 1
        "#,
    )
    .bind(episode_id)
    .fetch_optional(db)
    .await?;

    let title = pres_title
        .and_then(|(t, c)| t.or(c))
        .or(episode_title)
        .unwrap_or_else(|| "Uten tittel".to_string());

    // 6. Hent kapitler
    let chapter_rows: Vec<(String, Option<String>)> = sqlx::query_as(
        r#"
        SELECT
            COALESCE(e.metadata->>'at', '00:00:00') AS at,
            e.metadata->>'title' AS title
        FROM edges e
        WHERE e.source_id = $1
          AND e.edge_type = 'chapter'
        ORDER BY e.metadata->>'at'
        "#,
    )
    .bind(episode_id)
    .fetch_all(db)
    .await?;

    let chapters: Vec<ChapterData> = chapter_rows
        .into_iter()
        .map(|(at, ch_title)| {
            let secs = parse_time_to_secs(&at);
            ChapterData {
                at_secs: secs,
                at_display: at,
                title: ch_title.unwrap_or_else(|| format!("Kapittel")),
            }
        })
        .collect();

    Ok(Some(EpisodePlayerData {
        title,
        podcast_title,
        audio_url,
        artwork_url,
        duration_secs,
        chapters,
        base_url,
    }))
}

/// Parse "HH:MM:SS" eller "MM:SS" til sekunder.
fn parse_time_to_secs(time_str: &str) -> i64 {
    let parts: Vec<&str> = time_str.split(':').collect();
    match parts.len() {
        3 => {
            let h: i64 = parts[0].parse().unwrap_or(0);
            let m: i64 = parts[1].parse().unwrap_or(0);
            let s: i64 = parts[2].parse().unwrap_or(0);
            h * 3600 + m * 60 + s
        }
        2 => {
            let m: i64 = parts[0].parse().unwrap_or(0);
            let s: i64 = parts[1].parse().unwrap_or(0);
            m * 60 + s
        }
        _ => 0,
    }
}

/// Escape HTML-tekst.
fn html_escape(s: &str) -> String {
    s.replace('&', "&amp;")
        .replace('<', "&lt;")
        .replace('>', "&gt;")
        .replace('"', "&quot;")
        .replace('\'', "&#39;")
}

/// Escape streng for JavaScript (inni enkle anførselstegn).
fn js_escape(s: &str) -> String {
    s.replace('\\', "\\\\")
        .replace('\'', "\\'")
        .replace('\n', "\\n")
        .replace('\r', "")
        .replace("</", "<\\/")
}

// =============================================================================
// HTML-rendering
// =============================================================================

fn render_player_html(data: &EpisodePlayerData) -> String {
    let title = html_escape(&data.title);
    let podcast_title = html_escape(&data.podcast_title);

    let artwork_html = if let Some(ref url) = data.artwork_url {
        format!(
            r#"<img class="artwork" src="{}" alt="{}" />"#,
            html_escape(url),
            title
        )
    } else {
        // Placeholder med podcast-ikon
        r#"<div class="artwork artwork-placeholder"><svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 3v10.55c-.59-.34-1.27-.55-2-.55-2.21 0-4 1.79-4 4s1.79 4 4 4 4-1.79 4-4V7h4V3h-6z"/></svg></div>"#.to_string()
    };

    let chapters_json = serde_json::to_string(&data.chapters).unwrap_or_else(|_| "[]".to_string());

    let chapters_html = if data.chapters.is_empty() {
        String::new()
    } else {
        let mut html = String::from(r#"<div class="chapters" id="chapters"><div class="chapters-toggle" onclick="toggleChapters()"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M4 6h16M4 12h16M4 18h7"/></svg><span>Kapitler</span><svg class="chevron" id="chevron" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M19 9l-7 7-7-7"/></svg></div><div class="chapters-list" id="chapters-list">"#);
        for (i, ch) in data.chapters.iter().enumerate() {
            html.push_str(&format!(
                r#"<button class="chapter-btn" data-index="{}" onclick="seekToChapter({})"><span class="chapter-time">{}</span><span class="chapter-title">{}</span></button>"#,
                i,
                ch.at_secs,
                html_escape(&ch.at_display),
                html_escape(&ch.title),
            ));
        }
        html.push_str("</div></div>");
        html
    };

    let duration_secs = data.duration_secs.unwrap_or(0);

    format!(
        r##"<!DOCTYPE html>
<html lang="no">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{title} — {podcast_title}</title>
<style>
*,*::before,*::after{{box-sizing:border-box}}
body{{
  margin:0;padding:0;
  font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;
  background:#fff;color:#1a1a2e;
  overflow-x:hidden;
}}
.player{{
  display:flex;flex-direction:column;
  max-width:640px;margin:0 auto;
  padding:16px;gap:12px;
}}
/* Horisontal layout på bredere skjermer */
.player-top{{
  display:flex;align-items:center;gap:16px;
}}
.artwork,.artwork-placeholder{{
  width:80px;height:80px;min-width:80px;
  border-radius:8px;object-fit:cover;
  background:#e8e8f0;
}}
.artwork-placeholder{{
  display:flex;align-items:center;justify-content:center;
  color:#9ca3af;
}}
.artwork-placeholder svg{{width:36px;height:36px}}
.meta{{
  flex:1;min-width:0;
}}
.meta h1{{
  margin:0;font-size:16px;font-weight:600;
  line-height:1.3;
  overflow:hidden;text-overflow:ellipsis;
  display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;
}}
.meta .podcast-name{{
  margin:4px 0 0;font-size:12px;color:#6b7280;
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
}}
/* Kontroller */
.controls{{
  display:flex;align-items:center;gap:12px;
}}
.play-btn{{
  width:44px;height:44px;min-width:44px;
  border:none;border-radius:50%;cursor:pointer;
  background:#2563eb;color:#fff;
  display:flex;align-items:center;justify-content:center;
  transition:background .15s,transform .1s;
}}
.play-btn:hover{{background:#1d4ed8}}
.play-btn:active{{transform:scale(0.95)}}
.play-btn:disabled{{background:#d1d5db;cursor:not-allowed}}
.play-btn svg{{width:20px;height:20px}}
/* Waveform */
.waveform-wrap{{
  flex:1;min-width:0;
}}
#waveform{{width:100%;height:48px}}
/* Tidsvisning */
.time-row{{
  display:flex;justify-content:space-between;
  font-size:11px;color:#9ca3af;
  padding:0 2px;margin-top:-4px;
}}
/* Progressbar fallback (før waveform er klar) */
.progress-bar{{
  width:100%;height:4px;background:#e5e7eb;border-radius:2px;
  cursor:pointer;position:relative;overflow:hidden;
}}
.progress-bar .fill{{
  height:100%;background:#2563eb;border-radius:2px;
  transition:width .1s linear;
}}
/* Kapitler */
.chapters{{margin-top:4px}}
.chapters-toggle{{
  display:flex;align-items:center;gap:6px;
  font-size:13px;color:#6b7280;cursor:pointer;
  padding:6px 0;border:none;background:none;width:100%;
}}
.chapters-toggle svg{{width:16px;height:16px}}
.chapters-toggle .chevron{{
  margin-left:auto;transition:transform .2s;
}}
.chapters-toggle .chevron.open{{transform:rotate(180deg)}}
.chapters-list{{
  display:none;flex-direction:column;gap:2px;
  max-height:240px;overflow-y:auto;
  padding:4px 0;
}}
.chapters-list.show{{display:flex}}
.chapter-btn{{
  display:flex;align-items:center;gap:8px;
  padding:8px 10px;border:none;background:none;
  cursor:pointer;border-radius:6px;text-align:left;
  font-size:13px;color:#374151;
  transition:background .1s;
}}
.chapter-btn:hover{{background:#f3f4f6}}
.chapter-btn.active{{background:#eff6ff;color:#2563eb}}
.chapter-time{{
  font-size:11px;color:#9ca3af;min-width:48px;
  font-variant-numeric:tabular-nums;
}}
.chapter-title{{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}}
/* Loading-spinner */
.spinner{{
  width:20px;height:20px;
  border:2.5px solid rgba(255,255,255,.3);
  border-top-color:#fff;border-radius:50%;
  animation:spin .6s linear infinite;
}}
@keyframes spin{{to{{transform:rotate(360deg)}}}}
/* Feilmelding */
.error{{font-size:12px;color:#ef4444;padding:4px 0}}
/* Kapittelmerkører på waveform */
.chapter-marker{{
  position:absolute;top:0;bottom:0;width:2px;
  background:rgba(37,99,235,.4);pointer-events:none;
  z-index:2;
}}
/* Responsiv: smalere skjermer */
@media(max-width:360px){{
  .player{{padding:12px}}
  .artwork,.artwork-placeholder{{width:64px;height:64px;min-width:64px}}
  .meta h1{{font-size:14px}}
  #waveform{{height:40px}}
  .play-btn{{width:40px;height:40px;min-width:40px}}
}}
</style>
</head>
<body>
<div class="player">
  <div class="player-top">
    {artwork_html}
    <div class="meta">
      <h1>{title}</h1>
      <p class="podcast-name">{podcast_title}</p>
    </div>
  </div>
  <div class="controls">
    <button class="play-btn" id="play-btn" disabled aria-label="Spill av">
      <div class="spinner"></div>
    </button>
    <div class="waveform-wrap">
      <div id="waveform"></div>
    </div>
  </div>
  <div class="time-row">
    <span id="current-time">0:00</span>
    <span id="total-time">0:00</span>
  </div>
  {chapters_html}
</div>

<script src="https://unpkg.com/wavesurfer.js@7/dist/wavesurfer.min.js"></script>
<script>
(function() {{
  'use strict';
  var audioUrl = '{audio_url_js}';
  var durationHint = {duration_secs};
  var chapters = {chapters_json};

  var playBtn = document.getElementById('play-btn');
  var currentTimeEl = document.getElementById('current-time');
  var totalTimeEl = document.getElementById('total-time');
  var waveformEl = document.getElementById('waveform');

  var playing = false;
  var ready = false;
  var ws = null;

  function formatTime(s) {{
    if (!s || !isFinite(s)) return '0:00';
    var h = Math.floor(s / 3600);
    var m = Math.floor((s % 3600) / 60);
    var sec = Math.floor(s % 60);
    if (h > 0) return h + ':' + String(m).padStart(2,'0') + ':' + String(sec).padStart(2,'0');
    return m + ':' + String(sec).padStart(2,'0');
  }}

  if (durationHint > 0) {{
    totalTimeEl.textContent = formatTime(durationHint);
  }}

  // Play/pause ikoner
  var iconPlay = '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M8 5v14l11-7z"/></svg>';
  var iconPause = '<svg viewBox="0 0 24 24" fill="currentColor"><rect x="6" y="4" width="4" height="16" rx="1"/><rect x="14" y="4" width="4" height="16" rx="1"/></svg>';

  function updatePlayBtn() {{
    playBtn.innerHTML = playing ? iconPause : iconPlay;
    playBtn.setAttribute('aria-label', playing ? 'Pause' : 'Spill av');
  }}

  try {{
    ws = WaveSurfer.create({{
      container: waveformEl,
      height: 48,
      waveColor: '#93c5fd',
      progressColor: '#2563eb',
      cursorColor: '#1d4ed8',
      cursorWidth: 2,
      barWidth: 2,
      barGap: 1,
      barRadius: 2,
      normalize: true,
      url: audioUrl,
      mediaControls: false,
    }});

    ws.on('ready', function() {{
      ready = true;
      playBtn.disabled = false;
      updatePlayBtn();
      totalTimeEl.textContent = formatTime(ws.getDuration());
      addChapterMarkers(ws.getDuration());
    }});

    ws.on('timeupdate', function(t) {{
      currentTimeEl.textContent = formatTime(t);
      updateActiveChapter(t);
    }});

    ws.on('play', function() {{ playing = true; updatePlayBtn(); }});
    ws.on('pause', function() {{ playing = false; updatePlayBtn(); }});
    ws.on('finish', function() {{ playing = false; updatePlayBtn(); }});

    ws.on('error', function() {{
      playBtn.disabled = false;
      updatePlayBtn();
      var err = document.createElement('p');
      err.className = 'error';
      err.textContent = 'Kunne ikke laste lydfilen';
      waveformEl.parentNode.appendChild(err);
    }});
  }} catch(e) {{
    // WaveSurfer ikke tilgjengelig — faller tilbake
    console.warn('WaveSurfer feil:', e);
    playBtn.disabled = false;
    updatePlayBtn();
  }}

  playBtn.addEventListener('click', function() {{
    if (ws) ws.playPause();
  }});

  // Kapitler
  function addChapterMarkers(totalDur) {{
    if (!chapters.length || !totalDur) return;
    var container = waveformEl;
    container.style.position = 'relative';
    chapters.forEach(function(ch) {{
      var pct = (ch.at_secs / totalDur) * 100;
      if (pct > 0 && pct < 100) {{
        var marker = document.createElement('div');
        marker.className = 'chapter-marker';
        marker.style.left = pct + '%';
        container.appendChild(marker);
      }}
    }});
  }}

  function updateActiveChapter(t) {{
    var btns = document.querySelectorAll('.chapter-btn');
    var activeIdx = -1;
    for (var i = chapters.length - 1; i >= 0; i--) {{
      if (t >= chapters[i].at_secs) {{ activeIdx = i; break; }}
    }}
    btns.forEach(function(btn, i) {{
      btn.classList.toggle('active', i === activeIdx);
    }});
  }}

  // Globale funksjoner for onclick i HTML
  window.seekToChapter = function(secs) {{
    if (ws && ready) {{
      ws.setTime(secs);
      if (!playing) ws.play();
    }}
  }};

  window.toggleChapters = function() {{
    var list = document.getElementById('chapters-list');
    var chevron = document.getElementById('chevron');
    if (list) {{
      list.classList.toggle('show');
      if (chevron) chevron.classList.toggle('open');
    }}
  }};
}})();
</script>
</body>
</html>"##,
        title = title,
        podcast_title = podcast_title,
        artwork_html = artwork_html,
        audio_url_js = js_escape(&data.audio_url),
        duration_secs = duration_secs,
        chapters_json = chapters_json,
        chapters_html = chapters_html,
    )
}
