use std::collections::HashMap;
use std::io::{Read, Write};
use std::net::{TcpListener, TcpStream};

use clap::Args as ClapArgs;

use crate::{api, config};

#[derive(ClapArgs)]
pub struct Args {}

/// `sprouts login` — browser-based sign-in.
///
/// Starts a loopback server on a free port, opens the web app at
/// `/auth/cli?port=<port>`, and waits for it to redirect back with a Privy
/// token. The CLI then exchanges that token at `/auth/verify` for a session,
/// which is saved to `~/.sprouts/config.json`.
pub fn run(_args: Args) -> anyhow::Result<()> {
    let mut config = config::load()?;
    let web_url = config.web_url();
    let api_url = config.api_url();

    let listener = TcpListener::bind("127.0.0.1:0")?;
    let port = listener.local_addr()?.port();
    let url = format!("{web_url}/auth/cli?port={port}");

    println!("Opening your browser to sign in…");
    println!("  {url}");
    if open::that(&url).is_err() {
        println!("\nCouldn't open a browser automatically — open the URL above.");
    }
    println!("\nWaiting for authentication…");

    let privy_token = wait_for_callback(&listener)?;

    println!("Verifying…");
    let session = api::verify(&api_url, &privy_token)
        .map_err(|e| anyhow::anyhow!("verification failed: {e}"))?;

    config.api_url = Some(api_url);
    config.web_url = Some(web_url);
    config.token = Some(session.token);
    config.refresh_token = Some(session.refresh_token);
    config.user_id = Some(session.user.id);
    config.email = session.user.email.clone();
    config::save(&config)?;

    match session.user.email.as_deref() {
        Some(email) if !email.is_empty() => println!("\n✓ Logged in as {email}"),
        _ => println!("\n✓ Logged in"),
    }
    println!("Session saved to {}", config::path()?.display());
    Ok(())
}

/// Accept connections until the web app hits `/callback` with the Privy token.
fn wait_for_callback(listener: &TcpListener) -> anyhow::Result<String> {
    for stream in listener.incoming() {
        let mut stream = stream?;
        let path = match read_request_path(&mut stream) {
            Some(p) => p,
            None => {
                respond(&mut stream, 400, "Bad Request");
                continue;
            }
        };

        if !path.starts_with("/callback") {
            // e.g. a favicon request — ignore and keep waiting.
            respond(&mut stream, 204, "");
            continue;
        }

        let query = path.split_once('?').map(|(_, q)| q).unwrap_or("");
        match parse_query(query).get("token") {
            Some(token) => {
                respond(&mut stream, 200, SUCCESS_HTML);
                return Ok(token.clone());
            }
            None => {
                respond(&mut stream, 400, "Missing token");
                anyhow::bail!("authentication callback did not include a token");
            }
        }
    }
    anyhow::bail!("no authentication callback received")
}

/// Read just the request line and return its path (e.g. `/callback?token=…`).
fn read_request_path(stream: &mut TcpStream) -> Option<String> {
    let mut buf = [0u8; 16 * 1024];
    let n = stream.read(&mut buf).ok()?;
    let request = String::from_utf8_lossy(&buf[..n]);
    let first_line = request.lines().next()?;
    first_line.split_whitespace().nth(1).map(str::to_owned)
}

fn parse_query(query: &str) -> HashMap<String, String> {
    query
        .split('&')
        .filter(|s| !s.is_empty())
        .filter_map(|pair| {
            let (k, v) = pair.split_once('=')?;
            let v = urlencoding::decode(v).ok()?.into_owned();
            Some((k.to_owned(), v))
        })
        .collect()
}

fn respond(stream: &mut TcpStream, status: u16, body: &str) {
    let reason = match status {
        200 => "OK",
        204 => "No Content",
        400 => "Bad Request",
        _ => "OK",
    };
    let response = format!(
        "HTTP/1.1 {status} {reason}\r\nContent-Type: text/html; charset=utf-8\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body}",
        body.len()
    );
    let _ = stream.write_all(response.as_bytes());
}

const SUCCESS_HTML: &str = "<!doctype html><meta charset=utf-8><title>sprouts</title>\
<body style=\"font-family:system-ui,sans-serif;background:#0a0a0a;color:#e5e5e5;display:grid;place-items:center;height:100vh;margin:0\">\
<div style=\"text-align:center\"><div style=\"font-size:48px\">🌱</div>\
<h1>You're signed in</h1><p style=\"color:#888\">Close this tab and return to your terminal.</p></div>";
