use chrono::{SecondsFormat, Utc};
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
use std::collections::BTreeSet;
use std::env;
use std::fs::{create_dir_all, OpenOptions};
use std::io::{Read, Write};
use std::path::Path;
use std::time::Instant;
use tiny_http::{Header, Method, Request, Response, Server, StatusCode};

const DEFAULT_ALLOWED_REPO: &str = "pikasTech/unidesk";
const DEFAULT_CREDENTIAL_REF: &str = "github:unidesk-dev";
const MAX_BODY_BYTES: usize = 64 * 1024;

const CAPABILITIES: &[&str] = &[
    "github.auth.status",
    "github.issue.list",
    "github.issue.read",
    "github.pr.list",
    "github.pr.read",
    "github.pr.create",
    "github.pr.comment.create",
    "github.pr.preflight.dry-run",
];

#[derive(Clone)]
struct Config {
    host: String,
    port: u16,
    credential_ref: String,
    credential_kind: String,
    credential_configured: bool,
    allowed_repos: BTreeSet<String>,
    log_file: Option<String>,
}

#[derive(Deserialize)]
struct Caller {
    plane: Option<String>,
    #[serde(rename = "taskId")]
    task_id: Option<String>,
    #[serde(rename = "queueId")]
    queue_id: Option<String>,
}

#[derive(Deserialize)]
struct BrokerRequest {
    #[serde(rename = "requestId")]
    request_id: Option<String>,
    caller: Option<Caller>,
    repo: Option<String>,
    operation: Option<String>,
    #[serde(rename = "dryRun")]
    dry_run: Option<bool>,
    params: Option<Value>,
}

impl BrokerRequest {
    fn caller_json(&self) -> Value {
        caller_json(self.caller.as_ref())
    }
}

#[derive(Serialize)]
struct AuditEvent {
    #[serde(rename = "requestId")]
    request_id: String,
    #[serde(rename = "observedAt")]
    observed_at: String,
    caller: Value,
    operation: String,
    repo: String,
    resource: Value,
    #[serde(rename = "dryRun")]
    dry_run: bool,
    #[serde(rename = "credentialRef")]
    credential_ref: String,
    #[serde(rename = "credentialKind")]
    credential_kind: String,
    #[serde(rename = "credentialValuePrinted")]
    credential_value_printed: bool,
    upstream: Value,
    status: u16,
    ok: bool,
    #[serde(rename = "failureKind")]
    failure_kind: Option<String>,
    #[serde(rename = "degradedReason")]
    degraded_reason: Option<String>,
    #[serde(rename = "runnerDisposition")]
    runner_disposition: String,
    retryable: bool,
    #[serde(rename = "durationMs")]
    duration_ms: u128,
    redaction: Value,
}

fn now_iso() -> String {
    Utc::now().to_rfc3339_opts(SecondsFormat::Millis, true)
}

fn env_string(name: &str, fallback: &str) -> String {
    env::var(name)
        .ok()
        .filter(|value| !value.trim().is_empty())
        .unwrap_or_else(|| fallback.to_string())
}

fn env_flag(name: &str, fallback: bool) -> bool {
    let raw = env::var(name).ok().filter(|value| !value.trim().is_empty());
    match raw.as_deref().map(str::to_ascii_lowercase).as_deref() {
        Some("1") | Some("true") | Some("yes") | Some("on") => true,
        Some("0") | Some("false") | Some("no") | Some("off") => false,
        _ => fallback,
    }
}

fn config_from_env() -> Config {
    let allowed_repos = env_string("AUTH_BROKER_ALLOWED_REPOS", DEFAULT_ALLOWED_REPO)
        .split(',')
        .map(str::trim)
        .filter(|value| !value.is_empty())
        .map(str::to_string)
        .collect::<BTreeSet<_>>();
    let credential_ref = env_string("AUTH_BROKER_GITHUB_CREDENTIAL_REF", DEFAULT_CREDENTIAL_REF);
    Config {
        host: env_string("HOST", "0.0.0.0"),
        port: env_string("PORT", "4291")
            .parse::<u16>()
            .ok()
            .filter(|port| *port > 0)
            .unwrap_or(4291),
        credential_ref,
        credential_kind: "github-rest-token-ref".to_string(),
        credential_configured: env_flag("AUTH_BROKER_GITHUB_CONFIGURED", false),
        allowed_repos,
        log_file: env::var("AUTH_BROKER_AUDIT_LOG")
            .ok()
            .filter(|value| !value.trim().is_empty()),
    }
}

fn json_response(value: Value, status: u16) -> Response<std::io::Cursor<Vec<u8>>> {
    let body = serde_json::to_vec_pretty(&value).unwrap_or_else(|_| b"{\"ok\":false}".to_vec());
    let header = Header::from_bytes(
        &b"Content-Type"[..],
        &b"application/json; charset=utf-8"[..],
    )
    .unwrap();
    Response::from_data(body)
        .with_status_code(StatusCode(status))
        .with_header(header)
}

fn text_response(text: &str, status: u16) -> Response<std::io::Cursor<Vec<u8>>> {
    Response::from_string(text.to_string()).with_status_code(StatusCode(status))
}

fn read_body(request: &mut Request) -> Result<String, String> {
    let mut body = String::new();
    request
        .as_reader()
        .take(MAX_BODY_BYTES as u64 + 1)
        .read_to_string(&mut body)
        .map_err(|error| error.to_string())?;
    if body.len() > MAX_BODY_BYTES {
        return Err("request body exceeds 65536 bytes".to_string());
    }
    Ok(body)
}

fn health_payload(config: &Config) -> Value {
    json!({
        "ok": true,
        "service": "auth-broker",
        "phase": "p0",
        "github": {
            "configured": config.credential_configured,
            "credentialRef": config.credential_ref,
            "credentialKind": config.credential_kind,
            "valuesPrinted": false
        },
        "capabilities": CAPABILITIES,
        "allowedRepos": config.allowed_repos.iter().collect::<Vec<_>>(),
        "redaction": {
            "valuesPrinted": false,
            "secretKeysBlocked": ["token", "secret", "authorization", "cookie"]
        }
    })
}

fn request_id(input: Option<&String>) -> String {
    input
        .filter(|value| !value.trim().is_empty())
        .cloned()
        .unwrap_or_else(|| format!("authbroker-{}", Utc::now().timestamp_millis()))
}

fn caller_json(caller: Option<&Caller>) -> Value {
    json!({
        "plane": caller.and_then(|value| value.plane.clone()).unwrap_or_else(|| "unknown".to_string()),
        "taskId": caller.and_then(|value| value.task_id.clone()),
        "queueId": caller.and_then(|value| value.queue_id.clone())
    })
}

fn operation_allowed(operation: &str) -> bool {
    CAPABILITIES.iter().any(|item| *item == operation) && operation != "github.pr.preflight.dry-run"
}

fn resource_from_params(operation: &str, params: Option<&Value>) -> Value {
    let Some(Value::Object(map)) = params else {
        return json!(null);
    };
    match operation {
        "github.issue.read" | "github.pr.read" | "github.pr.comment.create" => {
            json!({ "number": map.get("number").and_then(Value::as_i64) })
        }
        "github.pr.create" => json!({
            "base": map.get("base").and_then(Value::as_str),
            "head": map.get("head").and_then(Value::as_str),
            "titleChars": map.get("title").and_then(Value::as_str).map(str::len),
            "bodyChars": map.get("body").and_then(Value::as_str).map(str::len)
        }),
        _ => json!(null),
    }
}

fn upstream_plan(operation: &str, repo: &str, resource: &Value) -> Value {
    let path = match operation {
        "github.auth.status" => "/rate_limit".to_string(),
        "github.issue.list" => format!("/repos/{repo}/issues"),
        "github.issue.read" => format!(
            "/repos/{repo}/issues/{}",
            resource
                .get("number")
                .and_then(Value::as_i64)
                .unwrap_or_default()
        ),
        "github.pr.list" => format!("/repos/{repo}/pulls"),
        "github.pr.read" => format!(
            "/repos/{repo}/pulls/{}",
            resource
                .get("number")
                .and_then(Value::as_i64)
                .unwrap_or_default()
        ),
        "github.pr.create" => format!("/repos/{repo}/pulls"),
        "github.pr.comment.create" => format!(
            "/repos/{repo}/issues/{}/comments",
            resource
                .get("number")
                .and_then(Value::as_i64)
                .unwrap_or_default()
        ),
        _ => "/unsupported".to_string(),
    };
    let method = match operation {
        "github.pr.create" | "github.pr.comment.create" => "POST",
        _ => "GET",
    };
    json!({ "method": method, "path": path })
}

fn failure_response(
    config: &Config,
    request_id: &str,
    failure_kind: &str,
    status: u16,
    runner_disposition: &str,
    retryable: bool,
    message: &str,
) -> Value {
    json!({
        "ok": false,
        "requestId": request_id,
        "failureKind": failure_kind,
        "degradedReason": message,
        "runnerDisposition": runner_disposition,
        "retryable": retryable,
        "message": message,
        "credential": {
            "credentialRef": config.credential_ref,
            "credentialKind": config.credential_kind,
            "configured": config.credential_configured,
            "valuesPrinted": false
        },
        "status": status,
        "next": [
            "configure broker-held credential reference outside runner env",
            "retry only after broker health reports github.configured=true"
        ],
        "redaction": {
            "valuesPrinted": false
        }
    })
}

fn dry_run_required(operation: &str, dry_run: bool) -> bool {
    matches!(operation, "github.pr.create" | "github.pr.comment.create") && !dry_run
}

fn handle_github(config: &Config, mut request: Request) {
    let start = Instant::now();
    let mut status = 200;
    let body = match read_body(&mut request) {
        Ok(body) => body,
        Err(message) => {
            let id = request_id(None);
            status = 400;
            let result = failure_response(
                config,
                &id,
                "validation-failed",
                status,
                "business-failed",
                false,
                &message,
            );
            let audit = audit_from_response(config, &result, start.elapsed().as_millis(), status);
            write_audit(config, &audit);
            let _ = request.respond(json_response(result, status));
            return;
        }
    };
    let parsed = serde_json::from_str::<BrokerRequest>(&body).map_err(|error| error.to_string());
    let result = match parsed {
        Ok(input) => {
            let id = request_id(input.request_id.as_ref());
            let repo = input
                .repo
                .clone()
                .unwrap_or_else(|| DEFAULT_ALLOWED_REPO.to_string());
            let operation = input.operation.clone().unwrap_or_default();
            let dry_run = input.dry_run.unwrap_or(false);
            let caller = input.caller_json();
            if operation.is_empty() {
                status = 400;
                failure_response(
                    config,
                    &id,
                    "validation-failed",
                    status,
                    "business-failed",
                    false,
                    "operation is required",
                )
            } else if !config.allowed_repos.contains(&repo) {
                status = 403;
                failure_response(
                    config,
                    &id,
                    "repo-not-allowed",
                    status,
                    "business-failed",
                    false,
                    "repo is not allowed",
                )
            } else if !operation_allowed(&operation) {
                status = 403;
                failure_response(
                    config,
                    &id,
                    "operation-not-allowed",
                    status,
                    "business-failed",
                    false,
                    "operation is not allowed",
                )
            } else if !config.credential_configured {
                status = 503;
                failure_response(
                    config,
                    &id,
                    "auth-not-configured",
                    status,
                    "infra-blocked",
                    false,
                    "broker GitHub credential reference is not configured",
                )
            } else if dry_run_required(&operation, dry_run) {
                status = 409;
                failure_response(
                    config,
                    &id,
                    "dry-run-required",
                    status,
                    "business-failed",
                    false,
                    "P0 mutation operations require dryRun=true",
                )
            } else {
                let resource = resource_from_params(&operation, input.params.as_ref());
                let upstream = upstream_plan(&operation, &repo, &resource);
                let writes_remote = false;
                json!({
                    "ok": true,
                    "requestId": id,
                    "runnerDisposition": "ready",
                    "failureKind": null,
                    "degradedReason": null,
                    "repo": repo,
                    "caller": caller,
                    "operation": operation,
                    "dryRun": dry_run,
                    "planned": true,
                    "writesRemote": writes_remote,
                    "credential": {
                        "credentialRef": config.credential_ref,
                        "credentialKind": config.credential_kind,
                        "configured": true,
                        "valuesPrinted": false
                    },
                    "upstream": upstream,
                    "resource": resource,
                    "audit": {
                        "credentialValuePrinted": false,
                        "redaction": { "valuesPrinted": false }
                    }
                })
            }
        }
        Err(message) => {
            let id = request_id(None);
            status = 400;
            failure_response(
                config,
                &id,
                "validation-failed",
                status,
                "business-failed",
                false,
                &message,
            )
        }
    };

    let audit = audit_from_response(config, &result, start.elapsed().as_millis(), status);
    write_audit(config, &audit);
    let _ = request.respond(json_response(result, status));
}

fn handle_pr_preflight(config: &Config, mut request: Request) {
    let start = Instant::now();
    let mut status = 200;
    let body = read_body(&mut request)
        .ok()
        .and_then(|body| serde_json::from_str::<Value>(&body).ok())
        .unwrap_or_else(|| json!({}));
    let id_source = body
        .get("requestId")
        .and_then(Value::as_str)
        .map(String::from);
    let id = request_id(id_source.as_ref());
    let repo = body
        .get("repo")
        .and_then(Value::as_str)
        .unwrap_or(DEFAULT_ALLOWED_REPO)
        .to_string();
    let result = if !config.allowed_repos.contains(&repo) {
        status = 403;
        failure_response(
            config,
            &id,
            "repo-not-allowed",
            status,
            "business-failed",
            false,
            "repo is not allowed",
        )
    } else if !config.credential_configured {
        status = 503;
        failure_response(
            config,
            &id,
            "auth-not-configured",
            status,
            "infra-blocked",
            false,
            "broker GitHub credential reference is not configured",
        )
    } else {
        let base = body.get("base").and_then(Value::as_str).unwrap_or("master");
        let head = body
            .get("head")
            .and_then(Value::as_str)
            .unwrap_or("<head-branch>");
        json!({
            "ok": true,
            "requestId": id,
            "runnerDisposition": "ready",
            "failureKind": null,
            "degradedReason": null,
            "tokenCoverage": {
                "ok": true,
                "source": "auth-broker",
                "scope": "broker-held-github-credential",
                "runnerEnvTokenRequired": false,
                "valuesPrinted": false
            },
            "prCapabilityContract": {
                "targetBranch": base,
                "headBranch": head,
                "systemGhBinaryRequiredForWrites": false,
                "preflightCreatesPr": false,
                "preflightMergesPr": false,
                "brokerProxy": {
                    "ok": true,
                    "operations": ["github.auth.status", "github.issue.read", "github.pr.read", "github.pr.create"],
                    "writesRemote": false
                },
                "pushDryRun": {
                    "runnerLocal": true,
                    "coveredByBroker": false
                }
            },
            "redaction": {
                "valuesPrinted": false
            }
        })
    };
    let audit = audit_from_response(config, &result, start.elapsed().as_millis(), status);
    write_audit(config, &audit);
    let _ = request.respond(json_response(result, status));
}

fn audit_from_response(
    config: &Config,
    response: &Value,
    duration_ms: u128,
    status: u16,
) -> AuditEvent {
    let request_id = response
        .get("requestId")
        .and_then(Value::as_str)
        .unwrap_or("unknown")
        .to_string();
    let operation = response
        .get("operation")
        .and_then(Value::as_str)
        .unwrap_or("github.pr.preflight.dry-run")
        .to_string();
    let repo = response
        .get("repo")
        .and_then(Value::as_str)
        .unwrap_or(DEFAULT_ALLOWED_REPO)
        .to_string();
    AuditEvent {
        request_id,
        observed_at: now_iso(),
        caller: response
            .get("caller")
            .cloned()
            .unwrap_or_else(|| json!({ "plane": "unknown", "taskId": null, "queueId": null })),
        operation,
        repo,
        resource: response
            .get("resource")
            .cloned()
            .unwrap_or_else(|| json!(null)),
        dry_run: response
            .get("dryRun")
            .and_then(Value::as_bool)
            .unwrap_or(true),
        credential_ref: config.credential_ref.clone(),
        credential_kind: config.credential_kind.clone(),
        credential_value_printed: false,
        upstream: response
            .get("upstream")
            .cloned()
            .unwrap_or_else(|| json!(null)),
        status,
        ok: response.get("ok").and_then(Value::as_bool).unwrap_or(false),
        failure_kind: response
            .get("failureKind")
            .and_then(Value::as_str)
            .map(str::to_string),
        degraded_reason: response
            .get("degradedReason")
            .and_then(Value::as_str)
            .map(str::to_string),
        runner_disposition: response
            .get("runnerDisposition")
            .and_then(Value::as_str)
            .unwrap_or("infra-blocked")
            .to_string(),
        retryable: response
            .get("retryable")
            .and_then(Value::as_bool)
            .unwrap_or(false),
        duration_ms,
        redaction: json!({ "valuesPrinted": false }),
    }
}

fn write_audit(config: &Config, event: &AuditEvent) {
    let Ok(line) = serde_json::to_string(event) else {
        return;
    };
    if let Some(path) = &config.log_file {
        if let Some(parent) = Path::new(path).parent() {
            let _ = create_dir_all(parent);
        }
        if let Ok(mut file) = OpenOptions::new().create(true).append(true).open(path) {
            let _ = writeln!(file, "{line}");
            return;
        }
    }
    println!("{line}");
}

fn route(config: &Config, request: Request) {
    let method = request.method().clone();
    let path = request.url().split('?').next().unwrap_or("/").to_string();
    match (method, path.as_str()) {
        (Method::Get, "/health") => {
            let _ = request.respond(json_response(health_payload(config), 200));
        }
        (Method::Post, "/v1/github/gh") => handle_github(config, request),
        (Method::Post, "/v1/github/pr-preflight") => handle_pr_preflight(config, request),
        _ => {
            let _ = request.respond(text_response("not found", 404));
        }
    }
}

fn main() {
    if env::args().any(|arg| arg == "--healthcheck") {
        println!(
            "{}",
            json!({
                "ok": true,
                "service": "auth-broker",
                "mode": "process-healthcheck",
                "credentialValuePrinted": false
            })
        );
        return;
    }
    let config = config_from_env();
    let address = format!("{}:{}", config.host, config.port);
    let server = Server::http(&address)
        .unwrap_or_else(|error| panic!("auth-broker listen failed on {address}: {error}"));
    println!(
        "{}",
        json!({
            "ok": true,
            "service": "auth-broker",
            "event": "started",
            "address": address,
            "credentialRef": config.credential_ref,
            "credentialValuePrinted": false
        })
    );
    for request in server.incoming_requests() {
        route(&config, request);
    }
}
