use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine};
use rand::Rng;
use sha2::{Digest, Sha256};
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
use tokio::net::TcpListener;
use url::Url;

use crate::config::TokenInfo;

const REDIRECT_URI: &str = "http://127.0.0.1:8901/callback";
const AUTH_URL: &str = "https://accounts.spotify.com/authorize";
const TOKEN_URL: &str = "https://accounts.spotify.com/api/token";

/// Spotify API scopes.
pub const SCOPES: &str = "\
user-read-playback-state \
user-modify-playback-state \
user-read-currently-playing \
user-read-recently-played \
user-library-read \
user-library-modify \
user-read-private \
user-read-email \
playlist-read-private \
playlist-read-collaborative \
playlist-modify-public \
playlist-modify-private \
streaming";

pub struct PkceAuth {
    pub client_id: String,
    code_verifier: String,
    code_challenge: String,
}

impl PkceAuth {
    pub fn new(client_id: &str) -> Self {
        let verifier = generate_code_verifier();
        let challenge = generate_code_challenge(&verifier);
        Self {
            client_id: client_id.to_string(),
            code_verifier: verifier,
            code_challenge: challenge,
        }
    }

    /// Build the Spotify authorization URL.
    pub fn auth_url(&self) -> String {
        format!(
            "{}?client_id={}&response_type=code&redirect_uri={}&scope={}&code_challenge_method=S256&code_challenge={}",
            AUTH_URL,
            urlencoding(&self.client_id),
            urlencoding(REDIRECT_URI),
            urlencoding(SCOPES),
            urlencoding(&self.code_challenge),
        )
    }

    /// Exchange an authorization code for tokens.
    pub async fn exchange_code(
        &self,
        http: &reqwest::Client,
        code: &str,
    ) -> Result<TokenInfo, String> {
        let params = [
            ("grant_type", "authorization_code"),
            ("code", code),
            ("redirect_uri", REDIRECT_URI),
            ("client_id", &self.client_id),
            ("code_verifier", &self.code_verifier),
        ];

        let resp = http
            .post(TOKEN_URL)
            .form(&params)
            .send()
            .await
            .map_err(|e| format!("Token exchange HTTP error: {e}"))?;

        if !resp.status().is_success() {
            let body = resp.text().await.unwrap_or_default();
            return Err(format!("Token exchange failed: {body}"));
        }

        let body: serde_json::Value = resp
            .json()
            .await
            .map_err(|e| format!("Token exchange parse error: {e}"))?;

        parse_token_response(&body)
    }
}

/// Refresh an access token using a refresh token.
pub async fn refresh_access_token(
    http: &reqwest::Client,
    client_id: &str,
    refresh_token: &str,
) -> Result<TokenInfo, String> {
    let params = [
        ("grant_type", "refresh_token"),
        ("refresh_token", refresh_token),
        ("client_id", client_id),
    ];

    let resp = http
        .post(TOKEN_URL)
        .form(&params)
        .send()
        .await
        .map_err(|e| format!("Token refresh HTTP error: {e}"))?;

    if !resp.status().is_success() {
        let body = resp.text().await.unwrap_or_default();
        return Err(format!("Token refresh failed: {body}"));
    }

    let mut body: serde_json::Value = resp
        .json()
        .await
        .map_err(|e| format!("Token refresh parse error: {e}"))?;

    // Spotify may not return a new refresh_token — preserve the old one
    if body.get("refresh_token").and_then(|v| v.as_str()).is_none() {
        body["refresh_token"] = serde_json::Value::String(refresh_token.to_string());
    }

    parse_token_response(&body)
}

/// Start a temporary HTTP server to receive the OAuth callback.
/// Returns the authorization code.
pub async fn wait_for_callback(timeout_secs: u64) -> Result<String, String> {
    let listener = TcpListener::bind("127.0.0.1:8901")
        .await
        .map_err(|e| format!("Failed to bind callback server: {e}"))?;

    let deadline = tokio::time::Instant::now() + tokio::time::Duration::from_secs(timeout_secs);

    loop {
        let accept = tokio::time::timeout_at(deadline, listener.accept()).await;
        let (mut stream, _) = match accept {
            Ok(Ok(s)) => s,
            Ok(Err(e)) => return Err(format!("Accept error: {e}")),
            Err(_) => return Err("Timed out waiting for Spotify authorisation.".into()),
        };

        let mut reader = BufReader::new(&mut stream);
        let mut request_line = String::new();
        if reader.read_line(&mut request_line).await.is_err() {
            continue;
        }

        // Parse GET /callback?code=XYZ HTTP/1.1
        let path = request_line
            .split_whitespace()
            .nth(1)
            .unwrap_or("")
            .to_string();

        // Drain remaining headers
        loop {
            let mut line = String::new();
            if reader.read_line(&mut line).await.is_err() || line.trim().is_empty() {
                break;
            }
        }

        if let Ok(url) = Url::parse(&format!("http://127.0.0.1:8901{}", path)) {
            if let Some(code) = url.query_pairs().find(|(k, _)| k == "code").map(|(_, v)| v.to_string()) {
                // Send success page
                let body = concat!(
                    "<html><body style='background:#0a0d15;color:#78B4FF;",
                    "font-family:Consolas;text-align:center;padding-top:80px'>",
                    "<h1>AUTHENTICATION COMPLETE</h1>",
                    "<p>Return to Lightify terminal.</p></body></html>"
                );
                let response = format!(
                    "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
                    body.len(), body
                );
                let _ = stream.write_all(response.as_bytes()).await;
                return Ok(code);
            }

            // Check for error
            if let Some(error) = url.query_pairs().find(|(k, _)| k == "error").map(|(_, v)| v.to_string()) {
                let body = format!(
                    "<html><body style='background:#0a0d15;color:#ff4141;\
                    font-family:Consolas;text-align:center;padding-top:80px'>\
                    <h1>AUTH ERROR</h1><p>{error}</p></body></html>"
                );
                let response = format!(
                    "HTTP/1.1 400 Bad Request\r\nContent-Type: text/html\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
                    body.len(), body
                );
                let _ = stream.write_all(response.as_bytes()).await;
                return Err(format!("Auth error: {error}"));
            }
        }

        // Not the callback path — respond and keep listening
        let body = "Not found";
        let response = format!(
            "HTTP/1.1 404 Not Found\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
            body.len(), body
        );
        let _ = stream.write_all(response.as_bytes()).await;
    }
}

// ── helpers ──

fn generate_code_verifier() -> String {
    let mut rng = rand::thread_rng();
    let bytes: Vec<u8> = (0..64).map(|_| rng.gen::<u8>()).collect();
    URL_SAFE_NO_PAD.encode(&bytes)
}

fn generate_code_challenge(verifier: &str) -> String {
    let hash = Sha256::digest(verifier.as_bytes());
    URL_SAFE_NO_PAD.encode(hash)
}

fn urlencoding(s: &str) -> String {
    url::form_urlencoded::byte_serialize(s.as_bytes()).collect()
}

fn parse_token_response(body: &serde_json::Value) -> Result<TokenInfo, String> {
    let access_token = body["access_token"]
        .as_str()
        .ok_or("Missing access_token")?
        .to_string();
    let refresh_token = body["refresh_token"]
        .as_str()
        .ok_or("Missing refresh_token")?
        .to_string();
    let expires_in = body["expires_in"].as_i64().unwrap_or(3600);
    let scope = body["scope"].as_str().unwrap_or(SCOPES).to_string();
    let token_type = body["token_type"]
        .as_str()
        .unwrap_or("Bearer")
        .to_string();

    let now = std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .unwrap_or_default()
        .as_secs() as i64;

    Ok(TokenInfo {
        access_token,
        token_type,
        expires_in,
        refresh_token,
        scope,
        expires_at: now + expires_in,
        cached_user_id: String::new(),
        cached_display_name: String::new(),
    })
}
