use std::collections::{HashMap, HashSet};
use std::net::{IpAddr, Ipv4Addr, SocketAddr, TcpListener};
use std::path::{Path, PathBuf};
use std::process::Stdio;
use std::time::Duration;

use futures_util::{SinkExt, StreamExt};
use serde::{Deserialize, Serialize};
use tokio::io::{AsyncBufReadExt, BufReader};
use tokio::process::{Child, Command};
use tokio::time::{sleep, timeout};
use tokio_tungstenite::tungstenite::Message;

use super::cookie::sanitize_device_id;
use super::types::BrowserAuth;
use crate::browser::locate_chromium_browser;
use crate::core::CliError;

const CDP_HOST: &str = "127.0.0.1";
const LOGIN_URL: &str = "https://suno.com/create";
const LOGIN_TIMEOUT: Duration = Duration::from_secs(300);
const POLL_INTERVAL: Duration = Duration::from_secs(2);

#[derive(Debug, Clone, Deserialize)]
struct CdpCookie {
    name: String,
    value: String,
    domain: String,
}

#[derive(Debug, Deserialize)]
struct CdpTarget {
    #[serde(rename = "type")]
    target_type: String,
    url: String,
    #[serde(rename = "webSocketDebuggerUrl")]
    web_socket_debugger_url: Option<String>,
}

#[derive(Serialize)]
struct CdpRequest<'a> {
    id: u64,
    method: &'a str,
    params: serde_json::Value,
}

type CdpStream =
    tokio_tungstenite::WebSocketStream<tokio_tungstenite::MaybeTlsStream<tokio::net::TcpStream>>;

struct CdpSession {
    ws: CdpStream,
    next_id: u64,
}

impl CdpSession {
    async fn connect(ws_url: &str) -> Result<Self, CliError> {
        let (ws, _) = tokio_tungstenite::connect_async(ws_url)
            .await
            .map_err(|e| CliError::Config(format!("CDP ws connect: {e}")))?;

        Ok(Self { ws, next_id: 0 })
    }

    async fn call(
        &mut self,
        method: &str,
        params: serde_json::Value,
    ) -> Result<serde_json::Value, CliError> {
        self.next_id += 1;
        let id = self.next_id;
        let payload = serde_json::to_string(&CdpRequest { id, method, params }).unwrap();

        self.ws
            .send(Message::Text(payload))
            .await
            .map_err(|e| CliError::Config(format!("CDP ws send {method}: {e}")))?;

        loop {
            let msg = timeout(Duration::from_secs(60), self.ws.next())
                .await
                .map_err(|_| CliError::Config(format!("CDP {method} timeout")))?
                .ok_or_else(|| CliError::Config(format!("CDP {method} ws closed")))?
                .map_err(|e| CliError::Config(format!("CDP {method} ws err: {e}")))?;

            let text = match msg {
                Message::Text(text) => text.to_string(),
                Message::Binary(_) | Message::Ping(_) | Message::Pong(_) | Message::Frame(_) => {
                    continue;
                }
                Message::Close(_) => {
                    return Err(CliError::Config(format!("CDP {method} ws closed mid-call")));
                }
            };

            let value: serde_json::Value = serde_json::from_str(&text)
                .map_err(|e| CliError::Config(format!("CDP {method} json: {e}")))?;
            if value.get("id").and_then(|id| id.as_u64()) == Some(id) {
                if let Some(err) = value.get("error") {
                    return Err(CliError::Config(format!("CDP {method} error: {err}")));
                }
                return Ok(value
                    .get("result")
                    .cloned()
                    .unwrap_or(serde_json::Value::Null));
            }
        }
    }
}

pub async fn extract_interactive_browser_auth() -> Result<BrowserAuth, CliError> {
    let port = allocate_cdp_port()?;
    let mut child = spawn_login_browser(port).await?;

    let result = async {
        wait_for_cdp(port).await?;
        let ws_url = find_or_create_login_tab(port).await?;
        eprintln!("Complete Suno login in the opened browser window...");
        wait_for_suno_auth(ws_url).await
    }
    .await;

    if result.is_ok() {
        eprintln!("Suno login captured; closing the dedicated browser window.");
    }
    let _ = child.start_kill();
    let _ = child.wait().await;
    result
}

pub fn delete_interactive_browser_profile() -> Result<(), CliError> {
    let profile_dir = interactive_browser_profile_dir()?;
    delete_interactive_browser_profile_at(&profile_dir)
}

async fn spawn_login_browser(port: u16) -> Result<Child, CliError> {
    let browser_path = locate_chromium_browser()?;
    let profile_dir = interactive_browser_profile_dir()?;
    std::fs::create_dir_all(&profile_dir)?;

    eprintln!(
        "Opening a dedicated browser profile for Suno login. This avoids reading your default browser cookies."
    );

    let mut child = Command::new(&browser_path)
        .arg(format!("--remote-debugging-port={port}"))
        .arg(format!("--user-data-dir={}", profile_dir.display()))
        .arg("--no-first-run")
        .arg("--no-default-browser-check")
        .arg("--disable-search-engine-choice-screen")
        .arg("--disable-features=TranslateUI")
        .arg("--window-size=1280,900")
        .arg(LOGIN_URL)
        .stdout(Stdio::null())
        .stderr(Stdio::piped())
        .spawn()
        .map_err(|e| {
            CliError::Config(format!("failed to spawn browser at {browser_path:?}: {e}"))
        })?;
    drain_stderr(&mut child);
    Ok(child)
}

fn interactive_browser_profile_dir() -> Result<PathBuf, CliError> {
    directories::ProjectDirs::from("com", "sunox", "sunox")
        .map(|d| d.data_dir().join("interactive-login-browser-profile"))
        .ok_or_else(|| CliError::Config("could not resolve data dir for browser profile".into()))
}

fn delete_interactive_browser_profile_at(profile_dir: &Path) -> Result<(), CliError> {
    if profile_dir.exists() {
        std::fs::remove_dir_all(profile_dir)?;
    }
    Ok(())
}

async fn wait_for_cdp(port: u16) -> Result<(), CliError> {
    for _ in 0..30 {
        if cdp_version(port).await.is_ok() {
            return Ok(());
        }
        sleep(Duration::from_millis(500)).await;
    }

    Err(CliError::Config(
        "Browser was spawned but never opened the CDP port. Check that Chrome or Edge can start normally, or set SUNO_BROWSER_PATH to a Chromium-family browser binary.".into(),
    ))
}

async fn cdp_version(port: u16) -> Result<serde_json::Value, CliError> {
    let url = format!("http://{CDP_HOST}:{port}/json/version");
    reqwest::Client::new()
        .get(&url)
        .timeout(Duration::from_secs(2))
        .send()
        .await
        .map_err(|e| CliError::Config(format!("CDP /json/version: {e}")))?
        .json()
        .await
        .map_err(|e| CliError::Config(format!("CDP json parse: {e}")))
}

async fn find_or_create_login_tab(port: u16) -> Result<String, CliError> {
    let targets = cdp_list(port).await?;
    if let Some(ws_url) = targets.into_iter().find_map(|target| {
        if target.target_type == "page" && !target.url.starts_with("chrome://") {
            target.web_socket_debugger_url
        } else {
            None
        }
    }) {
        return Ok(ws_url);
    }

    let url = format!("http://{CDP_HOST}:{port}/json/new?{}", urlencode(LOGIN_URL));
    let target: CdpTarget = reqwest::Client::new()
        .put(&url)
        .timeout(Duration::from_secs(10))
        .send()
        .await
        .map_err(|e| CliError::Config(format!("CDP /json/new: {e}")))?
        .json()
        .await
        .map_err(|e| CliError::Config(format!("CDP /json/new parse: {e}")))?;
    target
        .web_socket_debugger_url
        .ok_or_else(|| CliError::Config("CDP /json/new did not return a websocket URL".into()))
}

async fn cdp_list(port: u16) -> Result<Vec<CdpTarget>, CliError> {
    let url = format!("http://{CDP_HOST}:{port}/json/list");
    reqwest::Client::new()
        .get(&url)
        .timeout(Duration::from_secs(5))
        .send()
        .await
        .map_err(|e| CliError::Config(format!("CDP /json/list: {e}")))?
        .json()
        .await
        .map_err(|e| CliError::Config(format!("CDP json parse: {e}")))
}

async fn wait_for_suno_auth(ws_url: String) -> Result<BrowserAuth, CliError> {
    let deadline = tokio::time::Instant::now() + LOGIN_TIMEOUT;
    let mut session = CdpSession::connect(&ws_url).await?;
    session
        .call("Network.enable", serde_json::json!({}))
        .await?;
    session.call("Page.enable", serde_json::json!({})).await?;
    session
        .call(
            "Page.navigate",
            serde_json::json!({
                "url": LOGIN_URL
            }),
        )
        .await?;

    loop {
        if tokio::time::Instant::now() >= deadline {
            return Err(CliError::Config(
                "Timed out waiting for Suno login in the dedicated browser window.".into(),
            ));
        }

        let result = session
            .call("Network.getAllCookies", serde_json::json!({}))
            .await?;
        let cookies: Vec<CdpCookie> =
            serde_json::from_value(result.get("cookies").cloned().unwrap_or_default())
                .map_err(|e| CliError::Config(format!("CDP cookie parse: {e}")))?;
        if let Some(auth) = browser_auth_from_cdp_cookies(cookies) {
            return Ok(auth);
        }
        sleep(POLL_INTERVAL).await;
    }
}

fn browser_auth_from_cdp_cookies(cookies: Vec<CdpCookie>) -> Option<BrowserAuth> {
    let mut selected: HashMap<String, CdpCookie> = HashMap::new();
    let mut clerk_client_cookie: Option<String> = None;
    let mut auth_domain_clerk: Option<String> = None;
    let mut device_id: Option<String> = None;

    for cookie in cookies {
        if !cookie.domain.contains("suno.com") || cookie.name.is_empty() || cookie.value.is_empty()
        {
            continue;
        }

        if cookie.name == "__client" {
            if cookie.domain.contains("auth.suno.com") {
                auth_domain_clerk = Some(cookie.value.clone());
            } else if clerk_client_cookie.is_none() {
                clerk_client_cookie = Some(cookie.value.clone());
            }
        }
        if cookie.name == "ajs_anonymous_id" && device_id.is_none() {
            device_id = sanitize_device_id(&cookie.value);
        }

        match selected.get(&cookie.name) {
            Some(existing)
                if existing.domain.contains("auth.suno.com")
                    || !cookie.domain.contains("auth.suno.com") => {}
            _ => {
                selected.insert(cookie.name.clone(), cookie);
            }
        }
    }

    let clerk_client_cookie = auth_domain_clerk.or(clerk_client_cookie)?;
    let mut emitted = HashSet::new();
    let mut header_parts = Vec::new();
    if let Some(client_cookie) = selected.get("__client") {
        header_parts.push(format!("__client={}", client_cookie.value));
        emitted.insert("__client".to_string());
    }

    let mut rest: Vec<_> = selected
        .into_values()
        .filter(|cookie| !emitted.contains(&cookie.name))
        .collect();
    rest.sort_by(|a, b| a.name.cmp(&b.name));
    header_parts.extend(
        rest.into_iter()
            .map(|cookie| format!("{}={}", cookie.name, cookie.value)),
    );

    Some(BrowserAuth {
        clerk_client_cookie,
        cookie_header: header_parts.join("; "),
        device_id,
    })
}

fn allocate_cdp_port() -> Result<u16, CliError> {
    for port in 9234..9260 {
        let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), port);
        if TcpListener::bind(addr).is_ok() {
            return Ok(port);
        }
    }
    Err(CliError::Config(
        "could not find an available local browser debugging port".into(),
    ))
}

fn drain_stderr(child: &mut Child) {
    if let Some(stderr) = child.stderr.take() {
        let mut reader = BufReader::new(stderr).lines();
        tokio::spawn(async move {
            while let Ok(Some(_)) = reader.next_line().await {
                // discard browser startup noise.
            }
        });
    }
}

fn urlencode(s: &str) -> String {
    s.replace(":", "%3A").replace("/", "%2F")
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn cdp_cookies_prefer_auth_domain_clerk_cookie() {
        let auth = browser_auth_from_cdp_cookies(vec![
            CdpCookie {
                name: "__client".into(),
                value: "suno-client".into(),
                domain: ".suno.com".into(),
            },
            CdpCookie {
                name: "__client".into(),
                value: "auth-client".into(),
                domain: "auth.suno.com".into(),
            },
            CdpCookie {
                name: "ajs_anonymous_id".into(),
                value: "%22device-123%22".into(),
                domain: ".suno.com".into(),
            },
        ])
        .expect("auth");

        assert_eq!(auth.clerk_client_cookie, "auth-client");
        assert_eq!(auth.device_id.as_deref(), Some("device-123"));
        assert!(auth.cookie_header.contains("__client=auth-client"));
        assert!(!auth.cookie_header.contains("__client=suno-client"));
        assert!(
            auth.cookie_header
                .contains("ajs_anonymous_id=%22device-123%22")
        );
    }

    #[test]
    fn cdp_cookies_ignore_non_suno_domains() {
        let auth = browser_auth_from_cdp_cookies(vec![
            CdpCookie {
                name: "__client".into(),
                value: "wrong-client".into(),
                domain: "example.com".into(),
            },
            CdpCookie {
                name: "sid".into(),
                value: "session".into(),
                domain: ".suno.com".into(),
            },
        ]);

        assert!(auth.is_none());
    }

    #[test]
    fn delete_profile_dir_removes_nested_profile_files() {
        let profile_dir = std::env::temp_dir().join(format!(
            "sunox-profile-delete-test-{}",
            uuid::Uuid::new_v4()
        ));
        std::fs::create_dir_all(profile_dir.join("Default")).expect("profile dir");
        std::fs::write(profile_dir.join("Default").join("Cookies"), "cookie db")
            .expect("cookie db");

        delete_interactive_browser_profile_at(&profile_dir).expect("delete profile");

        assert!(!profile_dir.exists());
    }
}
