use std::collections::HashMap;
use std::path::{Path, PathBuf};
use pijul_srv_lite::parse_url;

use tiny_http::{Header, Response, StatusCode};

const SERVER_NAME: &str = "pijul-srv-lite/0.1.0";

fn main() {
    let port = std::env::var("PIJUL_SRV_PORT")
        .ok()
        .and_then(|p| p.parse().ok())
        .unwrap_or(8080);

    let repo_base = std::env::var("PIJUL_SRV_REPO_DIR")
        .map(PathBuf::from)
        .unwrap_or_else(|_| PathBuf::from("/vcs/pijul"));

    let repo_path = if repo_base.join(".pijul").exists() {
        repo_base
    } else {
        repo_base.join("default")
    };

    if !repo_path.join(".pijul").exists() {
        eprintln!("Repository not found at {:?}", repo_path);
        std::process::exit(1);
    }

    let server = tiny_http::Server::http(format!("0.0.0.0:{port}")).unwrap();

    println!("{SERVER_NAME} listening on port {port}");
    println!("Serving repository from {:?}", repo_path);

    for request in server.incoming_requests() {
        if request.method() == &tiny_http::Method::Head {
            let _ = request.respond(Response::empty(200));
            continue;
        }

        let response = handle_request(request.url(), &repo_path);
        let _ = request.respond(response);
    }
}


fn handle_request(url: &str, repo_path: &Path) -> Response<std::io::Cursor<Vec<u8>>> {
    let (path, query) = parse_url(url);

    if !path.starts_with(".pijul") {
        return not_found("not found");
    }

    if query.is_empty() {
        return serve_info(repo_path);
    }

    if let Some(change) = query.get("change") {
        return serve_change(repo_path, change);
    }

    if query.get("changelist").map(String::as_str) == Some("0")
        && let Some(channel) = query.get("channel")
    {
        return serve_changelist(repo_path, channel);
    }

    if let Some(channel) = query.get("channel") {
        if query.contains_key("id") {
            return serve_channel_id(repo_path, channel);
        }
        return serve_channel(repo_path, channel);
    }

    if query.contains_key("state") {
        return serve_state(repo_path);
    }

    if query.contains_key("identities") {
        return serve_identities();
    }

    if query.contains_key("log") {
        return serve_info(repo_path);
    }

    if query.contains_key("tag") {
        return serve_tag(repo_path);
    }

    bad_request()
}

/* ---------------- protocol handlers ---------------- */

fn serve_channel_id(repo_path: &Path, _channel: &str) -> Response<std::io::Cursor<Vec<u8>>> {
    let path = repo_path.join(".pijul").join("pristine").join("db");

    serve_file(&path, "application/octet-stream")
}

fn serve_channel(repo_path: &Path, _channel: &str) -> Response<std::io::Cursor<Vec<u8>>> {
    let path = repo_path.join(".pijul").join("pristine").join("db");

    serve_file(&path, "application/octet-stream")
}

fn serve_changelist(repo_path: &Path, channel: &str) -> Response<std::io::Cursor<Vec<u8>>> {
    let path = repo_path.join("changelists").join(channel);

    serve_file(&path, "text/plain")
}

fn serve_change(repo_path: &Path, hash: &str) -> Response<std::io::Cursor<Vec<u8>>> {
    if hash.len() < 3 || !hash.chars().all(|c| c.is_ascii_alphanumeric()) {
        return not_found("invalid change hash");
    }

    let (pfx, sfx) = hash.split_at(2);
    let path = repo_path
        .join(".pijul")
        .join("changes")
        .join(pfx)
        .join(format!("{}.change", sfx));

    serve_file(&path, "application/octet-stream")
}

fn serve_state(repo_path: &Path) -> Response<std::io::Cursor<Vec<u8>>> {
    let path = repo_path.join(".pijul").join("pristine").join("db");

    serve_file(&path, "application/octet-stream")
}

fn serve_tag(repo_path: &Path) -> Response<std::io::Cursor<Vec<u8>>> {
    let path = repo_path.join(".pijul").join("pristine").join("db");

    serve_file(&path, "application/octet-stream")
}

fn serve_identities() -> Response<std::io::Cursor<Vec<u8>>> {
    ok_json(r#"{"id":[],"rev":0}"#)
}

fn serve_info(repo_path: &Path) -> Response<std::io::Cursor<Vec<u8>>> {
    let mut out = String::from("{");

    let path = repo_path.join("changelists");
    if let Ok(entries) = std::fs::read_dir(path) {
        let names: Vec<String> = entries
            .filter_map(|e| e.ok().and_then(|e| e.file_name().into_string().ok()))
            .map(|s| format!("\"{s}\""))
            .collect();

        if !names.is_empty() {
            out.push_str("\"channels\":[");
            out.push_str(&names.join(","));
            out.push_str("],");
        }
    }

    out.push_str("\"head\":null}");
    ok_json(&out)
}

/* ---------------- helpers ---------------- */

fn serve_file(path: &Path, ty: &str) -> Response<std::io::Cursor<Vec<u8>>> {
    match std::fs::read(path) {
        Ok(data) => Response::from_data(data)
            .with_status_code(StatusCode(200))
            .with_header(Header::from_bytes("Content-Type", ty).unwrap())
            .with_header(
                Header::from_bytes("Cache-Control", "public, max-age=31536000, immutable").unwrap(),
            ),
        Err(_) => not_found("not found"),
    }
}


/* ---------------- responses ---------------- */

fn ok_bin(data: Vec<u8>) -> Response<std::io::Cursor<Vec<u8>>> {
    Response::from_data(data)
        .with_status_code(StatusCode(200))
        .with_header(Header::from_bytes("Content-Type", "application/octet-stream").unwrap())
}

fn ok_text(s: String) -> Response<std::io::Cursor<Vec<u8>>> {
    Response::from_data(s)
        .with_status_code(StatusCode(200))
        .with_header(Header::from_bytes("Content-Type", "text/plain").unwrap())
}

fn ok_json(s: &str) -> Response<std::io::Cursor<Vec<u8>>> {
    Response::from_string(s)
        .with_status_code(StatusCode(200))
        .with_header(Header::from_bytes("Content-Type", "application/json").unwrap())
}

fn bad_request() -> Response<std::io::Cursor<Vec<u8>>> {
    Response::from_string("bad request\n").with_status_code(StatusCode(400))
}

fn not_found(msg: &str) -> Response<std::io::Cursor<Vec<u8>>> {
    Response::from_string(format!("{msg}\n")).with_status_code(StatusCode(404))
}

fn internal_error() -> Response<std::io::Cursor<Vec<u8>>> {
    Response::from_string("internal error\n").with_status_code(StatusCode(500))
}