import albedo/openai_api/types
import gleam/dict.{type Dict}
import gleam/dynamic/decode
import gleam/json
import gleam/list
import gleam/option.{type Option, None, Some}
import gleam/result
import gleam/string
import gleam/uri

const not_configured = "provider is not configured; run /login"

const invalid_config = "provider configuration is invalid; run /login"

pub type Provider {
  Provider(
    name: String,
    extension: String,
    model: String,
    protocol: types.Protocol,
  )
}

/// Each saved profile decodes on its own, so one malformed entry makes only
/// that profile unusable.
type Configuration {
  Configuration(active: String, providers: Dict(String, Result(Provider, Nil)))
}

pub fn active(home: String) -> Result(Provider, String) {
  use config <- result.try(load(home))
  configured(config, config.active)
}

pub fn named(home: String, name: String) -> Result(Provider, String) {
  use config <- result.try(load(home))
  configured(config, name)
  |> result.replace_error("session provider is not configured; run /login")
}

/// Every valid saved profile, by name.
pub fn providers(home: String) -> Result(List(Provider), String) {
  profiles(home) |> result.map(result.values)
}

/// Every saved profile by name: usable, or the profile name and why not.
pub fn profiles(
  home: String,
) -> Result(List(Result(Provider, #(String, String))), String) {
  use config <- result.map(load(home))
  config.providers
  |> dict.keys
  |> list.sort(string.compare)
  |> list.map(fn(name) {
    configured(config, name) |> result.map_error(fn(error) { #(name, error) })
  })
}

// A migrated flat config remains "default" even if login selected a new provider.
pub fn legacy(home: String) -> Result(Provider, String) {
  use config <- result.try(load(home))
  case dict.has_key(config.providers, "default") {
    True -> configured(config, "default")
    False -> configured(config, config.active)
  }
}

fn configured(config: Configuration, name: String) -> Result(Provider, String) {
  use provider <- result.try(
    dict.get(config.providers, name)
    |> result.replace_error("active provider is not configured; run /login"),
  )
  use provider <- result.try(
    provider
    |> result.replace_error(invalid_config),
  )
  case
    string.trim(name) == ""
    || string.trim(provider.extension) == ""
    || string.trim(provider.model) == ""
  {
    True -> Error(invalid_config)
    False -> Ok(Provider(..provider, name: name))
  }
}

fn load(home: String) -> Result(Configuration, String) {
  use bytes <- result.try(
    read_config(home)
    |> result.replace_error(not_configured),
  )
  json.parse_bits(bytes, configuration_decoder())
  |> result.replace_error(invalid_config)
}

fn configuration_decoder() -> decode.Decoder(Configuration) {
  decode.one_of(named_decoder(), or: [legacy_decoder()])
}

fn named_decoder() -> decode.Decoder(Configuration) {
  use active <- decode.optional_field("active", "", decode.string)
  use providers <- decode.field(
    "providers",
    decode.dict(
      decode.string,
      decode.dynamic
        |> decode.map(fn(value) {
          decode.run(value, provider_decoder()) |> result.replace_error(Nil)
        }),
    ),
  )
  decode.success(Configuration(active, providers))
}

fn legacy_decoder() -> decode.Decoder(Configuration) {
  provider_decoder()
  |> decode.map(fn(provider) {
    Configuration("default", dict.from_list([#("default", Ok(provider))]))
  })
}

fn provider_decoder() -> decode.Decoder(Provider) {
  use provider_extension <- decode.optional_field(
    "extension",
    "openai",
    decode.string,
  )
  use model <- decode.field("model", decode.string)
  use protocol <- decode.field("protocol", types.protocol_decoder())
  decode.success(Provider("", provider_extension, model, protocol))
}

/// Decode one provider's extension-owned settings without exposing credentials
/// through the generic configuration type.
pub fn settings(
  home: String,
  name: String,
  decoder: decode.Decoder(a),
) -> Result(a, String) {
  use bytes <- result.try(
    read_config(home)
    |> result.replace_error(not_configured),
  )
  use root <- result.try(
    json.parse_bits(bytes, decode.dict(decode.string, decode.dynamic))
    |> result.replace_error(invalid_config),
  )
  let value = case dict.get(root, "providers") {
    Ok(providers) ->
      decode.run(providers, decode.dict(decode.string, decode.dynamic))
      |> result.replace_error(invalid_config)
      |> result.try(fn(providers) {
        dict.get(providers, name)
        |> result.replace_error(not_configured)
      })
    Error(_) if name == "default" ->
      json.parse_bits(bytes, decode.dynamic)
      |> result.replace_error(invalid_config)
    Error(_) -> Error(not_configured)
  }
  use value <- result.try(value |> result.replace_error(not_configured))
  decode.run(value, decoder)
  |> result.replace_error(invalid_config)
}

@external(erlang, "albedo_settings", "write_default")
pub fn select_default(
  home: String,
  provider: String,
  model: String,
) -> Result(Nil, String)

@external(erlang, "albedo_daemon", "read_config")
fn read_config(home: String) -> Result(BitArray, Nil)

/// Validate profiles at the daemon boundary without reporting secret values.
pub fn validate_profile(
  name: String,
  profile: String,
) -> Result(String, String) {
  let invalid = "invalid provider name, model, protocol, endpoint, or API key"
  use profile <- result.try(
    json.parse(profile, saved_profile_decoder())
    |> result.replace_error(invalid),
  )
  let model = string.trim(profile.model)
  let endpoint = string.trim(profile.endpoint)
  let key_valid = case profile.api_key {
    None -> True
    Some(key) -> !has_control_characters(key, including_space: True)
  }
  case
    valid_provider_name(name)
    && string.byte_size(model) > 0
    && string.byte_size(model) <= 512
    && !has_control_characters(model, including_space: False)
    && profile.extension != ""
    && valid_endpoint(endpoint, profile.extension)
    && key_valid
  {
    False -> Error(invalid)
    True -> {
      let fields = [
        #("extension", json.string(profile.extension)),
        #("baseUrl", json.string(trim_endpoint_slashes(endpoint))),
        #("model", json.string(model)),
        #("protocol", json.string(types.protocol_name(profile.protocol))),
      ]
      let fields = case profile.api_key {
        None -> fields
        Some(key) -> [#("apiKey", json.string(key)), ..fields]
      }
      Ok(json.object(fields) |> json.to_string)
    }
  }
}

type SavedProfile {
  SavedProfile(
    extension: String,
    endpoint: String,
    model: String,
    protocol: types.Protocol,
    api_key: Option(String),
  )
}

fn saved_profile_decoder() -> decode.Decoder(SavedProfile) {
  use extension <- decode.optional_field("extension", "openai", decode.string)
  use endpoint <- decode.optional_field("baseUrl", "", decode.string)
  use model <- decode.field("model", decode.string)
  use protocol <- decode.field("protocol", types.protocol_decoder())
  use api_key <- decode.optional_field(
    "apiKey",
    None,
    decode.string |> decode.map(Some),
  )
  decode.success(SavedProfile(extension, endpoint, model, protocol, api_key))
}

fn valid_provider_name(name: String) -> Bool {
  case string.to_utf_codepoints(name) {
    [] -> False
    [first, ..rest] ->
      string.byte_size(name) <= 64
      && ascii_alphanumeric(string.utf_codepoint_to_int(first))
      && list.all(rest, fn(codepoint) {
        let value = string.utf_codepoint_to_int(codepoint)
        ascii_alphanumeric(value) || value == 46 || value == 95 || value == 45
      })
  }
}

fn ascii_alphanumeric(value: Int) -> Bool {
  value >= 48
  && value <= 57
  || value >= 65
  && value <= 90
  || value >= 97
  && value <= 122
}

fn has_control_characters(
  value: String,
  including_space include_space: Bool,
) -> Bool {
  value
  |> string.to_utf_codepoints
  |> list.any(fn(codepoint) {
    let value = string.utf_codepoint_to_int(codepoint)
    value < 32 || value == 127 || include_space && value == 32
  })
}

fn valid_endpoint(endpoint: String, extension: String) -> Bool {
  case endpoint {
    "" -> extension != "openai"
    _ -> {
      // uri.parse lowercases the scheme; saved profiles require its original
      // spelling to be http or https, as the previous validator did.
      let scheme_valid =
        string.starts_with(endpoint, "http:")
        || string.starts_with(endpoint, "https:")
      case uri.parse(endpoint) {
        Ok(parsed) ->
          scheme_valid
          && parsed.host != None
          && parsed.host != Some("")
          && parsed.userinfo == None
          && parsed.query == None
          && parsed.fragment == None
        Error(_) -> False
      }
    }
  }
}

fn trim_endpoint_slashes(endpoint: String) -> String {
  case string.ends_with(endpoint, "/") {
    True -> trim_endpoint_slashes(string.remove_suffix(endpoint, "/"))
    False -> endpoint
  }
}
