use std::path::PathBuf;

use ts_rs::{Config, TS};

use crate::{
    Alert, AlertLevel, AlertSource, CommandEvent, CommandSnapshot, CommandStatus, CommandStream,
    CommandStreamSlice, CompletionEntry, CompletionKind, ErrorCode, Event, Greeting, InFlightBlock,
    InFlightSnapshot, InFlightToolCallState, InternalWorkerKind, InternalWorkerRef,
    InternalWorkerSnapshot, InvokeKind, MemoryWorkerEvent, Method, Permission, RewindSummary,
    RewindTarget, RewindTargetId, RunResult, ScopeRule, Segment, TurnResult, WorkerEvent,
    WorkerStatus,
    subscription::{
        EventSubscriptionSelector, SubscriptionEvent, SubscriptionEventPayload, SubscriptionFrame,
        SubscriptionFramePayload, SubscriptionId, SubscriptionRejectionCode, SubscriptionRequest,
        SubscriptionRequestId, SubscriptionResponse, SubscriptionSnapshot,
        SubscriptionTerminationCode, SubscriptionWorkdir, SubscriptionWorkdirId,
        SubscriptionWorker, SubscriptionWorkerId, SubscriptionWorkerIds,
        SubscriptionWorkerProtocolMethod, SubscriptionWorkerState,
    },
};

const GENERATED_RELATIVE_PATH: &str = "../../web/workspace/src/lib/generated/protocol.ts";

/// Repository-relative destination for the Workspace web protocol bindings.
pub fn generated_typescript_path() -> PathBuf {
    PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(GENERATED_RELATIVE_PATH)
}

/// Render Workspace web TypeScript bindings for the Worker wire protocol DTOs.
///
/// Rust DTOs in this crate remain the source of truth; this function is used by
/// both the checked-in artifact generator and the stale-output drift test.
pub fn generated_protocol_types() -> String {
    let cfg = Config::new().with_large_int("number");
    let mut output = String::from(
        "// @generated by `cargo run -p protocol --example generate_typescript --features typescript`\n\
         // Do not edit by hand. Rust DTO authority lives in `crates/protocol`.\n\
         // Large integer fields are JSON numbers on the wire and are emitted as TypeScript `number`.\n\n",
    );

    push_decl::<AlertLevel>(&cfg, &mut output);
    push_decl::<AlertSource>(&cfg, &mut output);
    push_decl::<CompletionKind>(&cfg, &mut output);
    push_decl::<WorkerStatus>(&cfg, &mut output);
    push_decl::<TurnResult>(&cfg, &mut output);
    push_decl::<InvokeKind>(&cfg, &mut output);
    push_decl::<RunResult>(&cfg, &mut output);
    push_decl::<ErrorCode>(&cfg, &mut output);
    push_decl::<Permission>(&cfg, &mut output);
    push_decl::<InFlightToolCallState>(&cfg, &mut output);
    push_decl::<CommandStatus>(&cfg, &mut output);
    push_decl::<CommandStream>(&cfg, &mut output);
    push_decl::<CommandStreamSlice>(&cfg, &mut output);
    push_decl::<CommandSnapshot>(&cfg, &mut output);
    push_decl::<CommandEvent>(&cfg, &mut output);
    push_decl::<ScopeRule>(&cfg, &mut output);
    push_decl::<CompletionEntry>(&cfg, &mut output);
    push_decl::<RewindTargetId>(&cfg, &mut output);
    push_decl::<RewindTarget>(&cfg, &mut output);
    push_decl::<RewindSummary>(&cfg, &mut output);
    push_decl::<InFlightBlock>(&cfg, &mut output);
    push_decl::<InFlightSnapshot>(&cfg, &mut output);
    push_decl::<InternalWorkerKind>(&cfg, &mut output);
    push_decl::<InternalWorkerRef>(&cfg, &mut output);
    push_decl::<InternalWorkerSnapshot>(&cfg, &mut output);
    push_decl::<Greeting>(&cfg, &mut output);
    push_decl::<Alert>(&cfg, &mut output);
    push_decl::<MemoryWorkerEvent>(&cfg, &mut output);
    push_decl::<Segment>(&cfg, &mut output);
    push_decl::<WorkerEvent>(&cfg, &mut output);
    push_decl::<SubscriptionRequestId>(&cfg, &mut output);
    push_decl::<SubscriptionId>(&cfg, &mut output);
    push_decl::<SubscriptionWorkerId>(&cfg, &mut output);
    push_decl::<SubscriptionWorkdirId>(&cfg, &mut output);
    push_decl::<SubscriptionWorkerIds>(&cfg, &mut output);
    push_decl::<SubscriptionWorkerState>(&cfg, &mut output);
    push_decl::<EventSubscriptionSelector>(&cfg, &mut output);
    push_decl::<SubscriptionWorker>(&cfg, &mut output);
    push_decl::<SubscriptionWorkdir>(&cfg, &mut output);
    push_decl::<SubscriptionSnapshot>(&cfg, &mut output);
    push_decl::<SubscriptionEventPayload>(&cfg, &mut output);
    push_decl::<SubscriptionRejectionCode>(&cfg, &mut output);
    push_decl::<SubscriptionTerminationCode>(&cfg, &mut output);
    push_decl::<SubscriptionRequest>(&cfg, &mut output);
    push_decl::<SubscriptionWorkerProtocolMethod>(&cfg, &mut output);
    push_decl::<SubscriptionResponse>(&cfg, &mut output);
    push_decl::<SubscriptionEvent>(&cfg, &mut output);
    push_decl::<SubscriptionFramePayload>(&cfg, &mut output);
    push_decl::<SubscriptionFrame>(&cfg, &mut output);
    push_decl::<Method>(&cfg, &mut output);
    push_decl::<Event>(&cfg, &mut output);

    normalize_typescript(output)
}

fn normalize_typescript(output: String) -> String {
    let mut lines = output.lines().map(str::trim_end).collect::<Vec<_>>();
    while lines.last() == Some(&"") {
        lines.pop();
    }
    lines.join("\n") + "\n"
}

fn push_decl<T: TS>(cfg: &Config, output: &mut String) {
    let decl = T::decl(cfg);
    output.push_str(&export_decl(&decl));
    output.push_str("\n\n");
}

fn export_decl(decl: &str) -> String {
    for prefix in ["type ", "interface ", "enum "] {
        if decl.starts_with(prefix) {
            return format!("export {decl}");
        }
    }
    decl.to_string()
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn generated_protocol_types_are_current() {
        let expected = generated_protocol_types();
        let path = generated_typescript_path();
        let actual = std::fs::read_to_string(&path)
            .unwrap_or_else(|err| panic!("failed to read {}: {err}", path.display()));
        assert_eq!(
            actual, expected,
            "generated TypeScript protocol bindings are stale; run `cargo run -p protocol --example generate_typescript --features typescript`"
        );
    }
}
