#![expect(
    clippy::disallowed_methods,
    reason = "build script: runs at compile time outside any runtime"
)]

use std::path::{Path, PathBuf};
use std::{env, fs};

use progenitor::{GenerationSettings, Generator, InterfaceStyle, TypeImpl};

/// Recursively convert OpenAPI 3.1 `type: "null"` patterns to 3.0 `nullable:
/// true`.
///
/// Handles two patterns:
/// - `oneOf: [{...}, {type: "null"}]` → the non-null schema with `nullable:
///   true`
/// - `type: [T1, ..., "null"]` → the remaining types with `nullable: true`
fn patch_nullable(value: &mut serde_json::Value) {
    match value {
        serde_json::Value::Object(map) => {
            // Pattern: oneOf with a {type: "null"} variant
            if let Some(one_of) = map.get_mut("oneOf") {
                if let Some(variants) = one_of.as_array_mut() {
                    let null_idx = variants.iter().position(|v| {
                        v.get("type").and_then(serde_json::Value::as_str) == Some("null")
                    });
                    if let Some(idx) = null_idx {
                        variants.remove(idx);
                        if variants.len() == 1 {
                            // Collapse single-variant oneOf into the schema itself
                            let mut inner = variants.remove(0);
                            if inner.get("$ref").is_some() {
                                inner = serde_json::json!({
                                    "allOf": [inner],
                                    "nullable": true,
                                });
                            } else {
                                inner
                                    .as_object_mut()
                                    .expect("oneOf collapse should leave an object schema")
                                    .insert("nullable".to_string(), serde_json::Value::Bool(true));
                            }
                            patch_nullable(&mut inner);
                            *value = inner;
                            return;
                        }
                        map.insert("nullable".to_string(), serde_json::Value::Bool(true));
                    }
                }
            }

            // Pattern: type array containing "null"
            let needs_nullable_from_type = map
                .get("type")
                .and_then(|v| v.as_array())
                .is_some_and(|arr| arr.iter().any(|v| v.as_str() == Some("null")));
            if needs_nullable_from_type {
                if let Some(type_val) = map.get_mut("type") {
                    if let Some(arr) = type_val.as_array_mut() {
                        arr.retain(|v| v.as_str() != Some("null"));
                        if arr.len() == 1 {
                            *type_val = arr.remove(0);
                        }
                    }
                }
                map.insert("nullable".to_string(), serde_json::Value::Bool(true));
            }

            for v in map.values_mut() {
                patch_nullable(v);
            }
        }
        serde_json::Value::Array(arr) => {
            for v in arr {
                patch_nullable(v);
            }
        }
        _ => {}
    }
}

/// Progenitor currently panics when an operation advertises more than one
/// request-body media type.
///
/// Keep the source OpenAPI spec accurate for docs, but collapse the
/// generated-client view down to a single preferred media type so code
/// generation can proceed.
fn patch_codegen_request_body_media_types(value: &mut serde_json::Value) {
    let Some(paths) = value
        .get_mut("paths")
        .and_then(serde_json::Value::as_object_mut)
    else {
        return;
    };

    for path_item in paths.values_mut() {
        let Some(item) = path_item.as_object_mut() else {
            continue;
        };

        for method in ["get", "put", "post", "delete", "patch"] {
            let Some(operation) = item
                .get_mut(method)
                .and_then(serde_json::Value::as_object_mut)
            else {
                continue;
            };
            let Some(content) = operation
                .get_mut("requestBody")
                .and_then(|request_body| request_body.get_mut("content"))
                .and_then(serde_json::Value::as_object_mut)
            else {
                continue;
            };
            if content.len() <= 1 {
                continue;
            }

            let preferred = content
                .get("application/octet-stream")
                .cloned()
                .map(|value| ("application/octet-stream".to_string(), value))
                .or_else(|| {
                    content
                        .iter()
                        .next()
                        .map(|(key, value)| (key.clone(), value.clone()))
                });
            if let Some((key, value)) = preferred {
                content.clear();
                content.insert(key, value);
            }
        }
    }
}

/// Progenitor currently panics when an operation advertises a typed success
/// response alongside a no-content success response. Keep the source spec
/// accurate for docs and other generators, but preserve the existing Rust
/// client shape for `DELETE /runs/{id}`: success with no body.
fn patch_codegen_delete_run_responses(value: &mut serde_json::Value) {
    let Some(responses) = value
        .get_mut("paths")
        .and_then(|paths| paths.get_mut("/api/v1/runs/{id}"))
        .and_then(|path| path.get_mut("delete"))
        .and_then(|operation| operation.get_mut("responses"))
        .and_then(serde_json::Value::as_object_mut)
    else {
        return;
    };
    responses.remove("200");
}

fn spec_path_from_manifest_dir(manifest_dir: &Path) -> PathBuf {
    manifest_dir
        .ancestors()
        .nth(3)
        .expect("fabro-api manifest dir should be nested under <repo>/lib/foundation/fabro-api")
        .join("docs/public/api-reference/fabro-api.yaml")
}

fn main() {
    let manifest_dir = env::var_os("CARGO_MANIFEST_DIR")
        .map(PathBuf::from)
        .expect("CARGO_MANIFEST_DIR should be set for build scripts");
    let spec_path = spec_path_from_manifest_dir(&manifest_dir);

    println!("cargo::rerun-if-changed={}", spec_path.display());

    let spec_text = fs::read_to_string(&spec_path)
        .unwrap_or_else(|e| panic!("failed to read {}: {e}", spec_path.display()));
    let mut spec_value: serde_json::Value =
        serde_yaml::from_str(&spec_text).unwrap_or_else(|e| panic!("failed to parse YAML: {e}"));

    // TODO: Remove 3.1→3.0 patch when progenitor supports OpenAPI 3.1.
    // Progenitor only supports OpenAPI 3.0.x; our spec uses 3.1.0 but doesn't
    // rely on any 3.1-only features that affect codegen.
    spec_value["openapi"] = serde_json::Value::String("3.0.3".to_string());
    patch_nullable(&mut spec_value);
    patch_codegen_request_body_media_types(&mut spec_value);
    patch_codegen_delete_run_responses(&mut spec_value);

    let spec: openapiv3::OpenAPI =
        serde_json::from_value(spec_value).expect("failed to deserialize OpenAPI spec");

    let mut settings = GenerationSettings::default();
    settings.with_interface(InterfaceStyle::Builder);
    let replacements: &[(&str, &str, &[TypeImpl])] = &[
        ("RunStatus", "fabro_types::status::RunStatus", &[]),
        ("PendingReason", "fabro_types::status::PendingReason", &[]),
        ("SuccessReason", "fabro_types::status::SuccessReason", &[]),
        ("FailureReason", "fabro_types::status::FailureReason", &[]),
        ("FailureCategory", "fabro_types::FailureCategory", &[]),
        ("FailureSignature", "fabro_types::FailureSignature", &[]),
        ("FailureDetail", "fabro_types::FailureDetail", &[]),
        ("RunFailure", "fabro_types::RunFailure", &[]),
        ("BlockedReason", "fabro_types::status::BlockedReason", &[]),
        (
            "RunControlAction",
            "fabro_types::status::RunControlAction",
            &[],
        ),
        ("Run", "fabro_types::Run", &[]),
        ("RunApproval", "fabro_types::RunApproval", &[]),
        ("RunApprovalState", "fabro_types::RunApprovalState", &[]),
        ("RunRunnableSource", "fabro_types::RunRunnableSource", &[]),
        ("RunSize", "fabro_types::RunSize", &[]),
        ("DiffSummary", "fabro_types::DiffSummary", &[]),
        ("RepositoryRef", "fabro_types::RepositoryRef", &[]),
        ("WorkflowSettings", "fabro_types::WorkflowSettings", &[]),
        // Run-level GitHub integration settings reuse the canonical resolved
        // types instead of generating parallel API DTOs; the wire shape is
        // identical (InterpString serializes as its source string).
        (
            "RunIntegrationsSettings",
            "fabro_types::settings::run::RunIntegrationsSettings",
            &[],
        ),
        (
            "RunIntegrationsGithubSettings",
            "fabro_types::settings::run::RunIntegrationsGithubSettings",
            &[],
        ),
        ("ServerSettings", "fabro_types::ServerSettings", &[]),
        (
            "ServerNamespace",
            "fabro_types::settings::ServerNamespace",
            &[],
        ),
        (
            "ServerListenSettings",
            "fabro_types::settings::server::ServerListenSettings",
            &[],
        ),
        (
            "ServerApiSettings",
            "fabro_types::settings::server::ServerApiSettings",
            &[],
        ),
        (
            "ServerWebSettings",
            "fabro_types::settings::server::ServerWebSettings",
            &[],
        ),
        (
            "ServerAuthSettings",
            "fabro_types::settings::server::ServerAuthSettings",
            &[],
        ),
        (
            "ServerAuthMethod",
            "fabro_types::settings::server::ServerAuthMethod",
            &[],
        ),
        (
            "ServerAuthGithubSettings",
            "fabro_types::settings::server::ServerAuthGithubSettings",
            &[],
        ),
        (
            "RunModelSettings",
            "fabro_types::settings::run::RunModelSettings",
            &[],
        ),
        (
            "RunModelControls",
            "fabro_types::settings::run::RunModelControls",
            &[],
        ),
        (
            "ServerSandboxSettings",
            "fabro_types::settings::server::ServerSandboxSettings",
            &[],
        ),
        (
            "ServerSandboxProvidersSettings",
            "fabro_types::settings::server::ServerSandboxProvidersSettings",
            &[],
        ),
        (
            "ServerSandboxProviderSettings",
            "fabro_types::settings::server::ServerSandboxProviderSettings",
            &[],
        ),
        (
            "ServerStorageSettings",
            "fabro_types::settings::server::ServerStorageSettings",
            &[],
        ),
        (
            "ServerArtifactsSettings",
            "fabro_types::settings::server::ServerArtifactsSettings",
            &[],
        ),
        (
            "ServerSlateDbSettings",
            "fabro_types::settings::server::ServerSlateDbSettings",
            &[],
        ),
        (
            "ObjectStoreSettings",
            "fabro_types::settings::server::ObjectStoreSettings",
            &[],
        ),
        (
            "ServerSchedulerSettings",
            "fabro_types::settings::server::ServerSchedulerSettings",
            &[],
        ),
        (
            "ServerLoggingSettings",
            "fabro_types::settings::server::ServerLoggingSettings",
            &[],
        ),
        (
            "LogDestination",
            "fabro_types::settings::server::LogDestination",
            &[],
        ),
        (
            "ServerIntegrationsSettings",
            "fabro_types::settings::server::ServerIntegrationsSettings",
            &[],
        ),
        (
            "GithubIntegrationSettings",
            "fabro_types::settings::server::GithubIntegrationSettings",
            &[],
        ),
        (
            "GithubIntegrationStrategy",
            "fabro_types::settings::server::GithubIntegrationStrategy",
            &[],
        ),
        (
            "SlackIntegrationSettings",
            "fabro_types::settings::server::SlackIntegrationSettings",
            &[],
        ),
        (
            "IntegrationWebhooksSettings",
            "fabro_types::settings::server::IntegrationWebhooksSettings",
            &[],
        ),
        (
            "WebhookStrategy",
            "fabro_types::settings::server::WebhookStrategy",
            &[],
        ),
        ("AuthMethod", "fabro_types::AuthMethod", &[]),
        ("IdpIdentity", "fabro_types::IdpIdentity", &[]),
        (
            "RunClientProvenance",
            "fabro_types::RunClientProvenance",
            &[],
        ),
        ("RunProvenance", "fabro_types::RunProvenance", &[]),
        (
            "RunServerProvenance",
            "fabro_types::RunServerProvenance",
            &[],
        ),
        ("Principal", "fabro_types::Principal", &[]),
        ("PrincipalUser", "fabro_types::UserPrincipal", &[]),
        ("SystemActorKind", "fabro_types::SystemActorKind", &[]),
        ("QuestionType", "fabro_types::QuestionType", &[]),
        ("StageCompletion", "fabro_types::StageCompletion", &[]),
        ("Conclusion", "fabro_types::Conclusion", &[]),
        ("StageOutcome", "fabro_types::StageOutcome", &[]),
        ("StageId", "fabro_types::StageId", &[]),
        ("ParallelBranchId", "fabro_types::ParallelBranchId", &[]),
        ("StageHandler", "fabro_types::StageHandler", &[]),
        ("StageState", "fabro_types::StageState", &[]),
        ("AgentControlState", "fabro_types::AgentControlState", &[]),
        ("CommandTermination", "fabro_types::CommandTermination", &[]),
        ("StageModelUsage", "fabro_types::StageModelUsage", &[]),
        (
            "ParallelBranchResult",
            "fabro_types::ParallelBranchResult",
            &[],
        ),
        ("StageProjection", "fabro_types::StageProjection", &[]),
        (
            "StageInferenceProjection",
            "fabro_types::StageInferenceProjection",
            &[],
        ),
        (
            "StageToolBatchProjection",
            "fabro_types::StageToolBatchProjection",
            &[],
        ),
        ("LlmOutputKind", "fabro_types::LlmOutputKind", &[]),
        ("PermissionLevel", "fabro_types::PermissionLevel", &[]),
        (
            "AgentSessionActivatedProps",
            "fabro_types::run_event::AgentSessionActivatedProps",
            &[],
        ),
        ("TodoListProjection", "fabro_types::TodoListProjection", &[]),
        ("SubAgentProjection", "fabro_types::SubAgentProjection", &[]),
        ("SubAgentStatus", "fabro_types::SubAgentStatus", &[]),
        ("SkillsProjection", "fabro_types::SkillsProjection", &[]),
        ("ActivatedSkill", "fabro_types::ActivatedSkill", &[]),
        ("AgentSkillSummary", "fabro_types::AgentSkillSummary", &[]),
        (
            "AgentSkillActivationSource",
            "fabro_types::AgentSkillActivationSource",
            &[],
        ),
        ("AgentToolSummary", "fabro_types::AgentToolSummary", &[]),
        ("AgentToolSource", "fabro_types::AgentToolSource", &[]),
        ("AgentToolCategory", "fabro_types::AgentToolCategory", &[]),
        (
            "AgentToolsAvailableProps",
            "fabro_types::AgentToolsAvailableProps",
            &[],
        ),
        (
            "McpServerProjection",
            "fabro_types::McpServerProjection",
            &[],
        ),
        ("McpServerStatus", "fabro_types::McpServerStatus", &[]),
        (
            "AgentMcpToolSummary",
            "fabro_types::AgentMcpToolSummary",
            &[],
        ),
        (
            "McpHttpProtocol",
            "fabro_types::settings::run::McpHttpProtocol",
            &[],
        ),
        (
            "McpTransport",
            "fabro_types::settings::run::McpTransport",
            &[],
        ),
        ("McpTransportView", "fabro_types::McpTransportView", &[]),
        ("StageContextWindow", "fabro_types::StageContextWindow", &[]),
        (
            "StageContextWindowProjection",
            "fabro_types::StageContextWindowProjection",
            &[],
        ),
        (
            "StageContextWindowBreakdownItem",
            "fabro_types::StageContextWindowBreakdownItem",
            &[],
        ),
        (
            "StageContextWindowCategory",
            "fabro_types::StageContextWindowCategory",
            &[],
        ),
        (
            "StageContextWindowCountMethod",
            "fabro_types::StageContextWindowCountMethod",
            &[],
        ),
        (
            "StageContextWindowStaleness",
            "fabro_types::StageContextWindowStaleness",
            &[],
        ),
        (
            "StageContextWindowUnavailableReason",
            "fabro_types::StageContextWindowUnavailableReason",
            &[],
        ),
        (
            "StageContextWindowWarning",
            "fabro_types::StageContextWindowWarning",
            &[],
        ),
        ("SecretMetadata", "fabro_types::SecretMetadata", &[]),
        ("Variable", "fabro_types::Variable", &[]),
        (
            "VariableListResponse",
            "fabro_types::VariableListResponse",
            &[],
        ),
        (
            "CreateVariableRequest",
            "fabro_types::CreateVariableRequest",
            &[],
        ),
        (
            "UpdateVariableRequest",
            "fabro_types::UpdateVariableRequest",
            &[],
        ),
        ("InterviewOption", "fabro_types::InterviewOption", &[]),
        ("ReviewTarget", "fabro_types::ReviewTarget", &[]),
        ("ReviewTargetKind", "fabro_types::ReviewTargetKind", &[]),
        (
            "InterviewQuestionRecord",
            "fabro_types::InterviewQuestionRecord",
            &[],
        ),
        (
            "PendingInterviewRecord",
            "fabro_types::PendingInterviewRecord",
            &[],
        ),
        ("CompletionUsage", "fabro_model::TokenCounts", &[]),
        ("BilledTokenCounts", "fabro_types::BilledTokenCounts", &[]),
        ("BillingModelRef", "fabro_model::ModelRef", &[]),
        ("BillingSpeed", "fabro_model::Speed", &[]),
        ("ExecOutputTail", "fabro_types::ExecOutputTail", &[]),
        ("StageTiming", "fabro_types::StageTiming", &[]),
        ("RunTiming", "fabro_types::RunTiming", &[]),
        ("ProviderId", "fabro_model::ProviderId", &[]),
        ("Model", "fabro_model::Model", &[]),
        ("Provider", "fabro_model::Provider", &[]),
        ("ModelLimits", "fabro_model::ModelLimits", &[]),
        (
            "ReasoningEffortFeature",
            "fabro_model::ReasoningEffortFeature",
            &[],
        ),
        ("ReasoningEffort", "fabro_model::ReasoningEffort", &[]),
        ("ModelFeatures", "fabro_model::ModelFeatures", &[]),
        ("ModelControls", "fabro_model::ModelControls", &[]),
        ("ModelCosts", "fabro_model::ModelCosts", &[]),
        ("ModelTestMode", "fabro_model::ModelTestMode", &[]),
        ("RunProjection", "fabro_types::RunProjection", &[]),
        ("RunEvent", "fabro_types::RunEvent", &[]),
        ("PairId", "fabro_types::PairId", &[]),
        ("PairMessageId", "fabro_types::PairMessageId", &[]),
        ("PairStatus", "fabro_types::PairStatus", &[]),
        ("PairTarget", "fabro_types::PairTarget", &[]),
        ("PairRecord", "fabro_types::PairRecord", &[]),
        (
            "RunPairStatusResponse",
            "fabro_types::RunPairStatusResponse",
            &[],
        ),
        ("PairStartRequest", "fabro_types::PairStartRequest", &[]),
        ("PairMessageRequest", "fabro_types::PairMessageRequest", &[]),
        ("PairMessageRecord", "fabro_types::PairMessageRecord", &[]),
        (
            "PairTranscriptResponse",
            "fabro_types::PairTranscriptResponse",
            &[],
        ),
        (
            "PairTranscriptEntry",
            "fabro_types::PairTranscriptEntry",
            &[],
        ),
        (
            "RunEventDetailResponse",
            "fabro_types::RunEventDetailResponse",
            &[],
        ),
        ("EventEnvelope", "fabro_types::EventEnvelope", &[]),
        ("PullRequest", "fabro_types::PullRequest", &[]),
        ("PullRequestLink", "fabro_types::PullRequestLink", &[]),
        (
            "PullRequestCreationId",
            "fabro_types::PullRequestCreationId",
            &[],
        ),
        (
            "PullRequestCreationStatus",
            "fabro_types::PullRequestCreationStatus",
            &[],
        ),
        (
            "PullRequestCreation",
            "fabro_types::PullRequestCreation",
            &[],
        ),
        ("PullRequestDetails", "fabro_types::PullRequestDetails", &[]),
        ("PullRequestMeta", "fabro_types::PullRequestMeta", &[]),
        (
            "PullRequestResponse",
            "fabro_types::PullRequestResponse",
            &[],
        ),
        (
            "PullRequestDetailsStatus",
            "fabro_types::PullRequestDetailsStatus",
            &[],
        ),
        (
            "PullRequestDetailsUnavailableReason",
            "fabro_types::PullRequestDetailsUnavailableReason",
            &[],
        ),
        ("RunSandboxRuntime", "fabro_types::RunSandboxRuntime", &[]),
        ("RunSandboxKind", "fabro_types::RunSandboxKind", &[]),
        ("RunSandboxPlan", "fabro_types::RunSandboxPlan", &[]),
        ("RunSandboxInstance", "fabro_types::RunSandboxInstance", &[]),
        ("RunSandboxFailure", "fabro_types::RunSandboxFailure", &[]),
        ("PullRequestUser", "fabro_types::PullRequestUser", &[]),
        ("PullRequestRef", "fabro_types::PullRequestRef", &[]),
        (
            "MergeMethod",
            "fabro_types::settings::run::MergeStrategy",
            &[],
        ),
        ("SecretType", "fabro_types::SecretType", &[]),
        ("DiffStats", "fabro_types::DiffStats", &[]),
        ("DirtyStatus", "fabro_types::DirtyStatus", &[]),
        ("GitContext", "fabro_types::GitContext", &[]),
        (
            "SandboxProviderKind",
            "fabro_types::SandboxProviderKind",
            &[],
        ),
        (
            "SystemIntegrationsResponse",
            "fabro_types::SystemIntegrationsResponse",
            &[],
        ),
        (
            "SystemIntegrationStatus",
            "fabro_types::SystemIntegrationStatus",
            &[],
        ),
        (
            "IntegrationProvider",
            "fabro_types::IntegrationProvider",
            &[],
        ),
        ("IntegrationStatus", "fabro_types::IntegrationStatus", &[]),
        (
            "IntegrationConnectionStatus",
            "fabro_types::IntegrationConnectionStatus",
            &[],
        ),
        (
            "IntegrationConnectionKind",
            "fabro_types::IntegrationConnectionKind",
            &[],
        ),
        (
            "IntegrationConnectionState",
            "fabro_types::IntegrationConnectionState",
            &[],
        ),
        ("RunSandbox", "fabro_types::RunSandbox", &[]),
        ("SandboxDetails", "fabro_types::SandboxDetails", &[]),
        ("SandboxInfo", "fabro_types::SandboxInfo", &[]),
        (
            "SandboxProviderLookupError",
            "fabro_types::SandboxProviderLookupError",
            &[],
        ),
        ("SandboxListMeta", "fabro_types::SandboxListMeta", &[]),
        (
            "SandboxListResponse",
            "fabro_types::SandboxListResponse",
            &[],
        ),
        ("SandboxNetwork", "fabro_types::SandboxNetwork", &[]),
        (
            "SandboxNetworkPolicy",
            "fabro_types::SandboxNetworkPolicy",
            &[],
        ),
        (
            "SandboxNetworkPolicyMode",
            "fabro_types::SandboxNetworkPolicyMode",
            &[],
        ),
        ("SandboxService", "fabro_types::SandboxService", &[]),
        (
            "SandboxServiceDiscoverySource",
            "fabro_types::SandboxServiceDiscoverySource",
            &[],
        ),
        (
            "SandboxServiceListMeta",
            "fabro_types::SandboxServiceListMeta",
            &[],
        ),
        (
            "SandboxServiceListResponse",
            "fabro_types::SandboxServiceListResponse",
            &[],
        ),
        ("SandboxState", "fabro_types::SandboxState", &[]),
        ("SandboxResources", "fabro_types::SandboxResources", &[]),
        ("SandboxTimestamps", "fabro_types::SandboxTimestamps", &[]),
        ("AskFabro", "fabro_types::AskFabro", &[]),
        ("Automation", "fabro_automation::Automation", &[]),
        ("AutomationRef", "fabro_types::AutomationRef", &[]),
        ("AutomationTarget", "fabro_automation::AutomationTarget", &[
        ]),
        (
            "AutomationTrigger",
            "fabro_automation::AutomationTrigger",
            &[],
        ),
        (
            "CreateAutomationRequest",
            "fabro_automation::AutomationDraft",
            &[],
        ),
        (
            "ReplaceAutomationRequest",
            "fabro_automation::AutomationReplace",
            &[],
        ),
        // MCP server catalog wire types reuse the domain model rather than
        // generating parallel DTOs. The integer formats in the spec generate as
        // `i64`/`i32`, but the domain types use `u64`/`u16`; the replacements
        // ensure the signed widths never leak into the public client.
        // (`McpTransport`/`McpTransportView`/`McpHttpProtocol` are mapped above.)
        //
        // Read responses use the value-omitting `McpServerView`/`McpTransportView`
        // projection (env/header values are never returned); write requests carry
        // the full transport, so they reuse the draft/replace domain types.
        ("McpServer", "fabro_types::McpServerView", &[]),
        ("CreateMcpServerRequest", "fabro_types::McpServerDraft", &[]),
        (
            "ReplaceMcpServerRequest",
            "fabro_types::McpServerReplace",
            &[],
        ),
        ("Environment", "fabro_environment::Environment", &[]),
        ("SessionId", "fabro_types::SessionId", &[]),
        ("TurnId", "fabro_types::TurnId", &[]),
        ("SessionStatus", "fabro_types::SessionStatus", &[]),
        ("SessionTurn", "fabro_types::SessionTurn", &[]),
        ("SessionMessage", "fabro_types::SessionMessage", &[]),
        ("SessionRecord", "fabro_types::SessionRecord", &[]),
        ("SessionSummary", "fabro_types::SessionSummary", &[]),
        ("SessionDetail", "fabro_types::SessionDetail", &[]),
        ("ReasoningOutput", "fabro_types::ReasoningOutput", &[]),
        ("CompletionMessage", "fabro_types::Message", &[]),
        ("CompletionMessageRole", "fabro_types::Role", &[]),
        ("CompletionContentPart", "fabro_types::ContentPart", &[]),
        ("WorkflowVersion", "fabro_types::WorkflowVersion", &[]),
        ("RunIntent", "fabro_types::RunIntent", &[]),
        ("RunIntentArgs", "fabro_types::RunIntentArgs", &[]),
        ("RunTarget", "fabro_types::RunTarget", &[]),
        ("GitRunTarget", "fabro_types::GitRunTarget", &[]),
        ("WorkflowPath", "fabro_types::WorkflowPath", &[]),
        ("WorkflowVersionId", "fabro_types::WorkflowVersionId", &[]),
        ("BlobHash", "fabro_types::BlobHash", &[]),
        ("CostSource", "fabro_model::CostSource", &[]),
    ];
    for (name, path, impls) in replacements {
        settings.with_replacement(*name, *path, impls.iter().copied());
    }

    let mut generator = Generator::new(&settings);
    let tokens = generator
        .generate_tokens(&spec)
        .expect("failed to generate tokens from OpenAPI spec");
    let syntax_tree = syn::parse2::<syn::File>(tokens).expect("failed to parse generated tokens");
    let formatted = prettyplease::unparse(&syntax_tree);

    let out_dir = env::var("OUT_DIR").expect("OUT_DIR should be set for build scripts");
    let out_path = Path::new(&out_dir).join("codegen.rs");
    fs::write(&out_path, formatted).expect("failed to write generated code");
}
