// SPDX-License-Identifier: MPL-2.0
// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell

//! HTTP client generation with pluggable HTTP backend

use crate::ir::{ApiSpec, Endpoint, HttpMethod, Parameter, ParameterLocation, RsType};
use super::{Config, ClientMode};
use anyhow::Result;
use heck::{ToLowerCamelCase, ToPascalCase};

pub fn generate(spec: &ApiSpec, config: &Config) -> Result<String> {
    let mut output = String::new();

    if config.client_mode == ClientMode::None {
        return Ok("".to_string());
    }

    // Header
    output.push_str("// SPDX-License-Identifier: MPL-2.0\n");
    output.push_str("// Generated by rescript-openapi - DO NOT EDIT\n");
    output.push_str(&format!("// Source: {} v{}\n\n", spec.title, spec.version));

    // Import core library and types
    output.push_str("open RescriptCore\n");
    if !config.unified_module {
        output.push_str(&format!("open {}Types\n", config.module_prefix));
        output.push_str(&format!("open {}Schema\n\n", config.module_prefix));
    } else {
        output.push_str("module S = RescriptSchema.S\n\n");
    }

    // HTTP abstraction layer
    output.push_str(r#"/** API error type */
type apiError =
  | HttpError({status: int, message: string, body: option<Js.Json.t>})
  | ParseError({message: string, body: option<Js.Json.t>})
  | HttpClientError({message: string})

/** HTTP method (polymorphic variant for Fetch API) */
type httpMethod = [#GET | #POST | #PUT | #PATCH | #DELETE | #HEAD | #OPTIONS]

/** HTTP request configuration */
type httpRequest = {
  method: httpMethod,
  url: string,
  headers: Dict.t<string>,
  body: option<Js.Json.t>,
}

/** HTTP client module signature - implement this to use any HTTP library */
module type HttpClient = {
  let request: httpRequest => promise<result<Js.Json.t, apiError>>
}
"#);

    if config.client_mode == ClientMode::Full {
        output.push_str(r#"
/** Default fetch-based HTTP client using @glennsl/rescript-fetch */
module FetchClient: HttpClient = {
  open Fetch

  let request = async (req: httpRequest): result<Js.Json.t, apiError> => {
    try {
      let init: Request.init = {
        method: (req.method :> Fetch.method),
        headers: Headers.fromObject(req.headers->Obj.magic),
      }
      let init = switch req.body {
      | Some(b) => {...init, body: b->JSON.stringify->Body.string}
      | None => init
      }
      let response = await fetch(req.url, init)

      if response->Response.ok {
        let json = await response->Response.json
        Ok(json)
      } else {
        let status = response->Response.status
        let message = response->Response.statusText
        let body = try {
          Some(await response->Response.json)
        } catch {
        | _ => None
        }
        Error(HttpError({status, message, body}))
      }
    } catch {
    | Exn.Error(e) => Error(HttpClientError({
        message: Exn.message(e)->Option.getOr("Network error"),
      }))
    }
  }
}
"#);
    }

    output.push_str(r#"
/** Authentication configuration */
type authConfig =
  | NoAuth
  | BearerToken(string)
  | ApiKey({key: string, headerName: string})

/** Client configuration */
type config = {
  baseUrl: string,
  headers: Dict.t<string>,
  auth: authConfig,
}

/** Create client configuration with optional authentication
 *
 * Bearer token auth:
 * ```rescript
 * let config = makeConfig(
 *   ~baseUrl="https://api.example.com",
 *   ~bearerToken="my-jwt-token",
 *   ()
 * )
 * ```
 *
 * API key auth:
 * ```rescript
 * let config = makeConfig(
 *   ~baseUrl="https://api.example.com",
 *   ~apiKey="my-api-key",
 *   ~apiKeyHeader="X-API-Key",
 *   ()
 * )
 * ```
 */
let makeConfig = (
  ~baseUrl: string,
  ~headers=Dict.make(),
  ~bearerToken: option<string>=?,
  ~apiKey: option<string>=?,
  ~apiKeyHeader: string="X-API-Key",
  ()
): config => {
  let auth = switch (bearerToken, apiKey) {
  | (Some(token), _) => BearerToken(token)
  | (_, Some(key)) => ApiKey({key, headerName: apiKeyHeader})
  | (None, None) => NoAuth
  }
  {
    baseUrl,
    headers,
    auth,
  }
}

/** Apply authentication headers to a headers dict */
let applyAuth = (headers: Dict.t<string>, auth: authConfig): unit => {
  switch auth {
  | NoAuth => ()
  | BearerToken(token) => headers->Dict.set("Authorization", `Bearer ${token}`)
  | ApiKey({key, headerName}) => headers->Dict.set(headerName, key)
  }
}

/** Build URL with query parameters */
let buildUrl = (baseUrl: string, path: string, query: Dict.t<string>): string => {
  let url = baseUrl ++ path
  let params = query
    ->Dict.toArray
    ->Array.map(((k, v)) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`)
    ->Array.join("&")

  if params->String.length > 0 {
    url ++ "?" ++ params
  } else {
    url
  }
}

/** API client functor - provide your own HttpClient implementation */
module Make = (Http: HttpClient) => {
"#);

    // Generate endpoint functions inside the functor
    for endpoint in &spec.endpoints {
        output.push_str(&generate_endpoint(endpoint));
        output.push('\n');
    }

    output.push_str("}\n\n");

    if config.client_mode == ClientMode::Full {
        // Default client using FetchClient
        output.push_str("/** Default client using fetch */\n");
        output.push_str("module Client = Make(FetchClient)\n\n");

        // Generate aliases map (operationId -> path-based name)
        output.push_str("/** Operation aliases for convenience */\n");
        output.push_str("module Aliases = {\n");
        for endpoint in &spec.endpoints {
            let alias = generate_path_alias(&endpoint.path, &endpoint.method);
            if alias != endpoint.operation_id {
                output.push_str(&format!("  let {} = Client.{}\n", alias, endpoint.operation_id));
            }
        }
        output.push_str("}\n");
    }

    Ok(output)
}

fn generate_endpoint(endpoint: &Endpoint) -> String {
    let mut output = String::new();

    // Documentation
    if let Some(doc) = &endpoint.doc {
        output.push_str(&format!("  /** {} */\n", doc));
    }

    let fn_name = &endpoint.operation_id;

    // Collect parameters by location
    let path_params: Vec<_> = endpoint.parameters.iter()
        .filter(|p| matches!(p.location, ParameterLocation::Path))
        .collect();
    let query_params: Vec<_> = endpoint.parameters.iter()
        .filter(|p| matches!(p.location, ParameterLocation::Query))
        .collect();
    let header_params: Vec<_> = endpoint.parameters.iter()
        .filter(|p| matches!(p.location, ParameterLocation::Header))
        .collect();

    // Build parameter list
    let mut params = vec!["config: config".to_string()];

    for p in &path_params {
        params.push(format!("~{}: {}", p.name, p.ty.to_rescript()));
    }

    if let Some(body) = &endpoint.request_body {
        params.push(format!("~body: {}", body.ty.to_rescript()));
    }

    // Optional query parameters
    for p in &query_params {
        if p.required {
            params.push(format!("~{}: {}", p.name, p.ty.to_rescript()));
        } else {
            params.push(format!("~{}=?", p.name));
        }
    }

    // Optional header parameters
    for p in &header_params {
        if p.required {
            params.push(format!("~{}: {}", p.name, p.ty.to_rescript()));
        } else {
            params.push(format!("~{}=?", p.name));
        }
    }

    // Determine return type
    let success_response = endpoint.responses.iter()
        .find(|r| r.status >= 200 && r.status < 300);

    let return_type = success_response
        .and_then(|r| r.ty.as_ref())
        .map(|t| t.to_rescript())
        .unwrap_or_else(|| "unit".to_string());

    output.push_str(&format!(
        "  let {} = async ({}, ()): result<{}, apiError> => {{\n",
        fn_name,
        params.join(", "),
        return_type
    ));

    // Build path with interpolation
    let path = build_path(&endpoint.path, &path_params);
    output.push_str(&format!("    let path = {}\n", path));

    // Build query dict
    output.push_str("    let query = Dict.make()\n");
    for p in &query_params {
        if p.required {
            output.push_str(&format!(
                "    query->Dict.set(\"{}\", {}->String.make)\n",
                p.name, p.name
            ));
        } else {
            output.push_str(&format!(
                "    switch {} {{ | Some(v) => query->Dict.set(\"{}\", v->String.make) | None => () }}\n",
                p.name, p.name
            ));
        }
    }

    // Build headers dict and apply authentication
    output.push_str("    let headers = Dict.fromArray(config.headers->Dict.toArray)\n");
    output.push_str("    headers->Dict.set(\"Content-Type\", \"application/json\")\n");
    output.push_str("    applyAuth(headers, config.auth)\n");

    for p in &header_params {
        if p.required {
            output.push_str(&format!(
                "    headers->Dict.set(\"{}\", {})\n",
                p.name, p.name
            ));
        } else {
            output.push_str(&format!(
                "    switch {} {{ | Some(v) => headers->Dict.set(\"{}\", v) | None => () }}\n",
                p.name, p.name
            ));
        }
    }

    // Build request body
    let body_expr = if let Some(body) = &endpoint.request_body {
        match &body.ty {
            RsType::Named(type_name) => {
                format!("Some(S.reverseConvertToJsonOrThrow(body, {}Schema))", type_name.to_lower_camel_case())
            }
            _ => "Some(body->Obj.magic)".to_string()
        }
    } else {
        "None".to_string()
    };

    // Make request (polymorphic variant for Fetch API)
    let method = match endpoint.method {
        HttpMethod::Get => "#GET",
        HttpMethod::Post => "#POST",
        HttpMethod::Put => "#PUT",
        HttpMethod::Patch => "#PATCH",
        HttpMethod::Delete => "#DELETE",
        HttpMethod::Head => "#HEAD",
        HttpMethod::Options => "#OPTIONS",
    };

    output.push_str(&format!(r#"
    let req: httpRequest = {{
      method: {},
      url: buildUrl(config.baseUrl, path, query),
      headers,
      body: {},
    }}

    switch await Http.request(req) {{
"#, method, body_expr));

    // Parse response
    if let Some(response) = success_response {
        if let Some(ty) = &response.ty {
            if let RsType::Named(type_name) = ty {
                output.push_str(&format!(
                    "    | Ok(json) => try {{\n      Ok(S.parseJsonOrThrow(json, {}Schema))\n    }} catch {{\n    | Exn.Error(e) => Error(ParseError({{message: Exn.message(e)->Option.getOr(\"Parse error\"), body: Some(json)}}))\n    }}\n",
                    type_name.to_lower_camel_case()
                ));
            } else {
                output.push_str("    | Ok(json) => Ok(json->Obj.magic)\n");
            }
        } else {
            output.push_str("    | Ok(_) => Ok()\n");
        }
    } else {
        output.push_str("    | Ok(json) => Ok(json->Obj.magic)\n");
    }

    output.push_str("    | Error(e) => Error(e)\n");
    output.push_str("    }\n");
    output.push_str("  }\n");

    output
}

fn build_path(path: &str, path_params: &[&Parameter]) -> String {
    if path_params.is_empty() {
        return format!("\"{}\"", path);
    }

    // Use template literal for path interpolation
    let mut template = path.to_string();

    for param in path_params {
        // Convert param to string based on type
        let param_expr = match &param.ty {
            RsType::String => param.name.clone(),
            RsType::Int => format!("{}->Int.toString", param.name),
            RsType::Float => format!("{}->Float.toString", param.name),
            RsType::Bool => format!("{}->Bool.toString", param.name),
            _ => format!("{}->String.make", param.name),
        };

        // Handle {param} style
        let placeholder = format!("{{{}}}", param.name);
        template = template.replace(&placeholder, &format!("${{{}}}", param_expr));

        // Handle :param style
        let placeholder_colon = format!(":{}", param.name);
        template = template.replace(&placeholder_colon, &format!("${{{}}}", param_expr));
    }

    format!("`{}`", template)
}

fn generate_path_alias(path: &str, method: &HttpMethod) -> String {
    let method_prefix = match method {
        HttpMethod::Get => "get",
        HttpMethod::Post => "create",
        HttpMethod::Put => "update",
        HttpMethod::Patch => "patch",
        HttpMethod::Delete => "delete",
        HttpMethod::Head => "head",
        HttpMethod::Options => "options",
    };

    // /users/{id}/posts -> UsersIdPosts -> getUsersIdPosts
    let path_parts: Vec<_> = path
        .split('/')
        .filter(|s| !s.is_empty() && !s.starts_with('{'))
        .collect();

    let path_name = path_parts
        .iter()
        .map(|s| s.to_pascal_case())
        .collect::<Vec<_>>()
        .join("");

    format!("{}{}", method_prefix, path_name)
}
