use std::fmt::Write;

use oa_forge_ir::*;

/// Emit only component schema type definitions (no endpoint-specific types).
pub fn emit_schemas(api: &ApiSpec, out: &mut String) -> Result<(), std::fmt::Error> {
    writeln!(out, "// Generated by oa-forge. Do not edit.")?;
    writeln!(out)?;

    for typedef in api.types.values() {
        emit_typedef(typedef, out)?;
        writeln!(out)?;
    }

    Ok(())
}

/// Emit endpoint-specific types for a single endpoint.
pub fn emit_endpoint(endpoint: &Endpoint, out: &mut String) -> Result<(), std::fmt::Error> {
    emit_endpoint_types(endpoint, out)
}

/// Emit TypeScript type definitions from the API spec.
pub fn emit(api: &ApiSpec, out: &mut String) -> Result<(), std::fmt::Error> {
    writeln!(out, "// Generated by oa-forge. Do not edit.")?;
    writeln!(out)?;

    // Emit component schemas as types/interfaces
    for typedef in api.types.values() {
        emit_typedef(typedef, out)?;
        writeln!(out)?;
    }

    // Emit endpoint request/response types
    for endpoint in &api.endpoints {
        emit_endpoint_types(endpoint, out)?;
    }

    Ok(())
}

fn emit_jsdoc(
    description: Option<&str>,
    indent: &str,
    out: &mut String,
) -> Result<(), std::fmt::Error> {
    if let Some(desc) = description {
        if desc.contains('\n') {
            writeln!(out, "{indent}/**")?;
            for line in desc.lines() {
                writeln!(out, "{indent} * {line}")?;
            }
            writeln!(out, "{indent} */")?;
        } else {
            writeln!(out, "{indent}/** {desc} */")?;
        }
    }
    Ok(())
}

/// Check if a type definition should use branded type pattern.
/// Branded types are emitted for simple primitives with a name that suggests an ID
/// (e.g., UserId, PostId) or when the format is uuid/uri/email.
fn should_brand(typedef: &TypeDef) -> bool {
    if !matches!(&typedef.repr, TypeRepr::Primitive(_)) {
        return false;
    }
    let name = &typedef.name;
    let format = typedef.format.as_deref().unwrap_or("");
    // Brand if: name ends with "Id" or format is a well-known identifier format
    name.ends_with("Id") || name.ends_with("ID") || format == "uuid" || format == "uri"
}

fn emit_typedef(typedef: &TypeDef, out: &mut String) -> Result<(), std::fmt::Error> {
    emit_jsdoc(typedef.description.as_deref(), "", out)?;

    // Branded type for ID-like primitives
    if should_brand(typedef) {
        let base = type_repr_to_ts(&typedef.repr);
        writeln!(
            out,
            "export type {} = {} & {{ readonly __brand: '{}' }};",
            typedef.name, base, typedef.name
        )?;
        return Ok(());
    }

    match &typedef.repr {
        TypeRepr::Object { properties } => {
            writeln!(out, "export interface {} {{", typedef.name)?;
            for prop in properties.values() {
                emit_jsdoc(prop.description.as_deref(), "  ", out)?;
                let optional = if prop.required { "" } else { "?" };
                let readonly = if prop.read_only { "readonly " } else { "" };
                let ts_type = type_repr_to_ts(&prop.repr);
                writeln!(out, "  {readonly}{}{}: {};", prop.name, optional, ts_type)?;
            }
            writeln!(out, "}}")?;
        }
        TypeRepr::Union { variants, .. } => {
            let types: Vec<String> = variants.iter().map(type_repr_to_ts).collect();
            writeln!(out, "export type {} = {};", typedef.name, types.join(" | "))?;
        }
        TypeRepr::Enum { values } => {
            let vals: Vec<String> = values
                .iter()
                .map(|v| match v {
                    EnumValue::String(s) => format!("'{s}'"),
                    EnumValue::Integer(n) => n.to_string(),
                })
                .collect();
            writeln!(out, "export type {} = {};", typedef.name, vals.join(" | "))?;
        }
        other => {
            writeln!(
                out,
                "export type {} = {};",
                typedef.name,
                type_repr_to_ts(other)
            )?;
        }
    }
    Ok(())
}

/// Emit a params interface (e.g. `FooPathParams`, `FooQueryParams`) for the given location.
/// When `use_optional` is true, non-required params get `?` suffix.
fn emit_params_interface(
    id: &str,
    suffix: &str,
    location: ParamLocation,
    use_optional: bool,
    endpoint: &Endpoint,
    out: &mut String,
) -> Result<(), std::fmt::Error> {
    let params: Vec<&EndpointParam> = endpoint
        .parameters
        .iter()
        .filter(|p| p.location == location)
        .collect();

    if params.is_empty() {
        return Ok(());
    }

    writeln!(out, "export interface {id}{suffix} {{")?;
    for p in &params {
        let optional = if use_optional && !p.required { "?" } else { "" };
        let ts = type_repr_to_ts(&p.repr);
        writeln!(out, "  {}{}: {};", p.name, optional, ts)?;
    }
    writeln!(out, "}}")?;
    writeln!(out)?;
    Ok(())
}

fn emit_endpoint_types(endpoint: &Endpoint, out: &mut String) -> Result<(), std::fmt::Error> {
    let id = &endpoint.operation_id;

    // Path params are always required (no optional marker)
    emit_params_interface(id, "PathParams", ParamLocation::Path, false, endpoint, out)?;
    emit_params_interface(id, "QueryParams", ParamLocation::Query, true, endpoint, out)?;
    emit_params_interface(
        id,
        "HeaderParams",
        ParamLocation::Header,
        true,
        endpoint,
        out,
    )?;
    emit_params_interface(
        id,
        "CookieParams",
        ParamLocation::Cookie,
        true,
        endpoint,
        out,
    )?;

    // Response type
    if let Some(response) = &endpoint.response {
        let ts = type_repr_to_ts(response);
        writeln!(out, "export type {id}Response = {ts};")?;
        writeln!(out)?;
    }

    // Request body type — use Partial<T> for PATCH with $ref bodies
    if let Some(body) = &endpoint.request_body {
        let ts = if endpoint.method == HttpMethod::Patch {
            match body {
                TypeRepr::Ref { name } => format!("Partial<{name}>"),
                other => type_repr_to_ts(other),
            }
        } else {
            type_repr_to_ts(body)
        };
        writeln!(out, "export type {id}Body = {ts};")?;
        writeln!(out)?;
    }

    // Error response type
    if let Some(error) = &endpoint.error_response {
        let ts = type_repr_to_ts(error);
        writeln!(out, "export type {id}Error = {ts};")?;
        writeln!(out)?;
    }

    Ok(())
}

fn type_repr_to_ts(repr: &TypeRepr) -> String {
    match repr {
        TypeRepr::Primitive(p) => match p {
            PrimitiveType::String => "string".to_string(),
            PrimitiveType::Number | PrimitiveType::Integer => "number".to_string(),
            PrimitiveType::Boolean => "boolean".to_string(),
        },
        TypeRepr::Object { properties } => {
            if properties.is_empty() {
                return "Record<string, unknown>".to_string();
            }
            let fields: Vec<String> = properties
                .values()
                .map(|p| {
                    let opt = if p.required { "" } else { "?" };
                    let readonly = if p.read_only { "readonly " } else { "" };
                    format!("{readonly}{}{}: {}", p.name, opt, type_repr_to_ts(&p.repr))
                })
                .collect();
            format!("{{ {} }}", fields.join("; "))
        }
        TypeRepr::Array { items } => format!("{}[]", type_repr_to_ts(items)),
        TypeRepr::Union { variants, .. } => {
            let types: Vec<String> = variants.iter().map(type_repr_to_ts).collect();
            types.join(" | ")
        }
        TypeRepr::Ref { name } => name.clone(),
        TypeRepr::Enum { values } => {
            let vals: Vec<String> = values
                .iter()
                .map(|v| match v {
                    EnumValue::String(s) => format!("'{s}'"),
                    EnumValue::Integer(n) => n.to_string(),
                })
                .collect();
            vals.join(" | ")
        }
        TypeRepr::Tuple { items } => {
            let parts: Vec<String> = items.iter().map(type_repr_to_ts).collect();
            format!("[{}]", parts.join(", "))
        }
        TypeRepr::Nullable(inner) => format!("{} | null", type_repr_to_ts(inner)),
        TypeRepr::Map { value } => format!("Record<string, {}>", type_repr_to_ts(value)),
        TypeRepr::Intersection { members } => {
            let parts: Vec<String> = members
                .iter()
                .map(|m| {
                    let ts = type_repr_to_ts(m);
                    // Wrap unions in parens for correct precedence: Base & (A | B)
                    if matches!(m, TypeRepr::Union { .. }) {
                        format!("({ts})")
                    } else {
                        ts
                    }
                })
                .collect();
            parts.join(" & ")
        }
        TypeRepr::Any => "unknown".to_string(),
    }
}
