use std::env;
use std::io::{Read, Write};
use std::net::TcpStream;
use std::time::{SystemTime, UNIX_EPOCH};

const SIGNING_CERT_NAME: &str = "homelab-oidc";
const PROVIDERS: &[&str] = &["grafana", "minio"];

fn main() {
    let base_url = env::var("AUTHENTIK_BASE_URL")
        .unwrap_or_else(|_| "http://localhost:7000".into());
    let token = env::var("AUTHENTIK_BOOTSTRAP_TOKEN")
        .expect("AUTHENTIK_BOOTSTRAP_TOKEN must be set");

    let host = parse_host(&base_url);

    let ts = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .unwrap()
        .as_secs();
    let new_name = format!("{}-{}", SIGNING_CERT_NAME, ts);

    let body = format!(
        r#"{{"common_name":"{}","validity_days":365,"key_size":4096}}"#,
        new_name
    );
    let resp = http(&host, &token, "POST", "/api/v3/crypto/certificatekeypairs/generate/", Some(&body));
    let new_pk = extract_str(&resp, "pk").expect("no pk in generate response");
    println!("created  '{}' pk={}", new_name, new_pk);

    for provider in PROVIDERS {
        let list = http(
            &host, &token, "GET",
            &format!("/api/v3/providers/oauth2/?name={}", provider),
            None,
        );
        let provider_pk = match extract_str(&list, "pk") {
            Some(pk) => pk,
            None => {
                eprintln!("WARN: provider '{}' not found — skipping", provider);
                continue;
            }
        };
        let patch = format!(r#"{{"signing_key":"{}"}}"#, new_pk);
        http(&host, &token, "PATCH",
            &format!("/api/v3/providers/oauth2/{}/", provider_pk),
            Some(&patch),
        );
        println!("rotated  '{}' → signing_key={}", provider, new_pk);
    }

    println!("rotation complete");
}

fn parse_host(base_url: &str) -> String {
    let stripped = base_url
        .trim_start_matches("http://")
        .trim_start_matches("https://");
    let host = stripped.split('/').next().unwrap_or(stripped);
    if host.contains(':') {
        host.to_string()
    } else {
        format!("{}:80", host)
    }
}

fn http(host: &str, token: &str, method: &str, path: &str, body: Option<&str>) -> String {
    let mut stream = TcpStream::connect(host)
        .unwrap_or_else(|e| panic!("connect {}: {}", host, e));

    let body_str = body.unwrap_or("");
    let hostname = host.split(':').next().unwrap_or(host);
    let req = format!(
        "{method} {path} HTTP/1.1\r\n\
         Host: {hostname}\r\n\
         Authorization: Bearer {token}\r\n\
         Content-Type: application/json\r\n\
         Content-Length: {len}\r\n\
         Connection: close\r\n\
         \r\n\
         {body_str}",
        len = body_str.len(),
    );
    stream.write_all(req.as_bytes()).unwrap();

    let mut raw = String::new();
    stream.read_to_string(&mut raw).unwrap();

    let (head, resp_body) = raw.split_once("\r\n\r\n").unwrap_or((&raw, ""));

    let status: u16 = head.lines().next()
        .and_then(|l| l.split_whitespace().nth(1))
        .and_then(|s| s.parse().ok())
        .unwrap_or(0);
    if status >= 400 {
        panic!("{} {} → HTTP {} — {}", method, path, status, resp_body.trim());
    }

    resp_body.to_string()
}

// Extracts the value of the first `"key": <value>` match in raw JSON.
// Handles both quoted strings ("pk": "uuid") and bare numbers ("pk": 5).
fn extract_str(json: &str, key: &str) -> Option<String> {
    let needle = format!("\"{}\":", key);
    let after_colon = json.find(&needle)? + needle.len();
    let rest = json[after_colon..].trim_start();
    if let Some(inner) = rest.strip_prefix('"') {
        Some(inner[..inner.find('"')?].to_string())
    } else {
        let end = rest.find(|c: char| c == ',' || c == '}' || c.is_ascii_whitespace())?;
        Some(rest[..end].to_string())
    }
}
