///|
struct ResolvedSkill {
  plugin_name : String
  plugin_path : String
  skill_name : String
  skill_path : String
}

///|
struct NormalizedPlugin {
  name : String
  source : String
  description : String?
}

///|
fn plugins_from_skills(skills : Array[ResolvedSkill]) -> Array[LockPlugin] {
  skills
  .iter()
  .fold(init=[], fn(plugins, skill) { upsert_plugin_skill(plugins, skill) })
}

///|
fn append_enabled_skill(plugin : LockPlugin, skill_name : String) -> LockPlugin {
  if plugin.enabled_skills.contains(skill_name) {
    plugin
  } else {
    {
      name: plugin.name,
      path: plugin.path,
      enabled_skills: plugin.enabled_skills
      .iter()
      .concat([skill_name].iter())
      .collect(),
    }
  }
}

///|
fn upsert_plugin_skill(
  plugins : Array[LockPlugin],
  skill : ResolvedSkill,
) -> Array[LockPlugin] {
  let updated = plugins.map(fn(plugin) {
    if plugin.name == skill.plugin_name {
      append_enabled_skill(plugin, skill.skill_name)
    } else {
      plugin
    }
  })
  if plugins.any(fn(plugin) { plugin.name == skill.plugin_name }) {
    updated
  } else {
    updated
    .iter()
    .concat(
      [
        {
          name: skill.plugin_name,
          path: skill.plugin_path,
          enabled_skills: [skill.skill_name],
        },
      ].iter(),
    )
    .collect()
  }
}

///|
fn merge_plugins(
  existing : Array[LockPlugin],
  incoming : Array[LockPlugin],
) -> Array[LockPlugin] {
  incoming.iter().fold(init=existing, merge_plugin)
}

///|
fn merge_plugin(
  plugins : Array[LockPlugin],
  incoming : LockPlugin,
) -> Array[LockPlugin] {
  let updated = plugins.map(fn(plugin) {
    if plugin.name == incoming.name {
      incoming.enabled_skills.iter().fold(init=plugin, append_enabled_skill)
    } else {
      plugin
    }
  })
  if plugins.any(fn(plugin) { plugin.name == incoming.name }) {
    updated
  } else {
    updated.iter().concat([incoming].iter()).collect()
  }
}

///|
enum MarketplaceKind {
  Claude
  Cursor
  Codex
} derive(Eq)

///|
struct MarketplaceInfo {
  kind : MarketplaceKind
  path : String
}

///|
fn marketplace_kind_from_string(value : String) -> MarketplaceKind raise {
  match value {
    "claude" => Claude
    "cursor" => Cursor
    "codex" => Codex
    _ => fail("unsupported marketplace kind: \{value}")
  }
}

///|
fn marketplace_kind_to_string(kind : MarketplaceKind) -> String {
  match kind {
    Claude => "claude"
    Cursor => "cursor"
    Codex => "codex"
  }
}

///|
fn marketplace_kinds() -> Array[MarketplaceKind] {
  [Claude, Cursor, Codex]
}

///|
fn marketplace_path(kind : MarketplaceKind) -> String {
  match kind {
    Claude => @path.Path::join(".claude-plugin", "marketplace.json").to_string()
    Cursor => @path.Path::join(".cursor-plugin", "marketplace.json").to_string()
    Codex =>
      (".agents"
      |> @path.Path::join("plugins")
      |> @path.Path::join("marketplace.json")).to_string()
  }
}

///|
fn marketplace_config_dir(kind : MarketplaceKind) -> String {
  match kind {
    Claude => ".claude-plugin"
    Cursor => ".cursor-plugin"
    Codex => ".codex-plugin"
  }
}

///|
async fn detect_marketplace(repo_dir : String) -> MarketplaceInfo? {
  let claude = @path.Path::join(repo_dir, marketplace_path(Claude)).to_string()
  let cursor = @path.Path::join(repo_dir, marketplace_path(Cursor)).to_string()
  let codex = @path.Path::join(repo_dir, marketplace_path(Codex)).to_string()
  match
    @async.all([
      () => @fs.exists(claude),
      () => @fs.exists(cursor),
      () => @fs.exists(codex),
    ]) {
    [true, _, _] => Some({ kind: Claude, path: claude })
    [false, true, _] => Some({ kind: Cursor, path: cursor })
    [false, false, true] => Some({ kind: Codex, path: codex })
    _ => None
  }
}

///|
pub async fn has_supported_plugin_format(dir : String) -> Bool {
  @async.all(
    marketplace_kinds().map(kind => {
      () => {
        @fs.exists(
          @path.Path::join(dir, marketplace_config_dir(kind)).to_string(),
        )
      }
    }),
  ).any(value => value)
}

///|
fn normalize_plugin_source(value : String?) -> String? {
  match value {
    Some(path) if path.has_prefix("./") => Some(path[2:].to_owned())
    _ => value
  }
}

///|
let marketplace_source_path_lens : @lens.PresenceLens[Json] = @lens.root()
  .json("path")
  .optional()

///|
let marketplace_plugin_name_lens : @lens.PresenceLens[Json] = @lens.root()
  .json("name")
  .optional()

///|
let marketplace_plugin_source_lens : @lens.PresenceLens[Json] = @lens.root()
  .json("source")
  .optional()

///|
let marketplace_plugin_description_lens : @lens.PresenceLens[Json] = @lens.root()
  .json("description")
  .optional()

///|
let marketplace_name_lens : @lens.PresenceLens[Json] = @lens.root()
  .json("name")
  .optional()

///|
let marketplace_plugins_lens : @lens.PresenceLens[Json] = @lens.root()
  .json("plugins")
  .optional()

///|
struct MarketplaceSourceWire {
  wire_string : String?
  wire_path : String?
}

///|
impl FromJson for MarketplaceSourceWire with fn from_json(json, path) {
  match json {
    Json::String(value) => { wire_string: Some(value), wire_path: None }
    _ => {
      let source_path = marketplace_source_path_lens.add_to_json_path(path)
      let path_value = marketplace_source_path_lens.get(json) catch {
        _ => None
      }
      let wire_path = match path_value {
        Some(value) =>
          Some((@json.from_json(value, path=source_path) : String)) catch {
            _ => None
          }
        None => None
      }
      { wire_string: None, wire_path }
    }
  }
}

///|
struct MarketplacePluginWire {
  wire_name : String?
  wire_source : MarketplaceSourceWire?
  wire_description : String?
}

///|
impl FromJson for MarketplacePluginWire with fn from_json(json, path) {
  guard json is Json::Object(_) else {
    raise @json.JsonDecodeError(
      (path, "MarketplacePluginWire: expected object"),
    )
  }
  let name_path = marketplace_plugin_name_lens.add_to_json_path(path)
  let source_path = marketplace_plugin_source_lens.add_to_json_path(path)
  let description_path = marketplace_plugin_description_lens.add_to_json_path(
    path,
  )
  let name_value = marketplace_plugin_name_lens.get(json) catch { _ => None }
  let wire_name = match name_value {
    Some(value) =>
      Some((@json.from_json(value, path=name_path) : String)) catch {
        _ => None
      }
    None => None
  }
  let source_value = marketplace_plugin_source_lens.get(json) catch {
    _ => None
  }
  let wire_source = match source_value {
    Some(value) =>
      Some((@json.from_json(value, path=source_path) : MarketplaceSourceWire)) catch {
        _ => None
      }
    None => None
  }
  let description_value = marketplace_plugin_description_lens.get(json) catch {
    _ => None
  }
  let wire_description = match description_value {
    Some(value) =>
      Some((@json.from_json(value, path=description_path) : String)) catch {
        _ => None
      }
    None => None
  }
  { wire_name, wire_source, wire_description }
}

///|
struct MarketplaceWire {
  wire_name : String?
  wire_plugins : Array[MarketplacePluginWire]?
}

///|
impl FromJson for MarketplaceWire with fn from_json(json, path) {
  let name_path = marketplace_name_lens.add_to_json_path(path)
  let plugins_path = marketplace_plugins_lens.add_to_json_path(path)
  let name_value = marketplace_name_lens.get(json) catch { _ => None }
  let wire_name = match name_value {
    Some(value) =>
      Some((@json.from_json(value, path=name_path) : String)) catch {
        _ => None
      }
    None => None
  }
  let plugins_value = marketplace_plugins_lens.get(json) catch { _ => None }
  let wire_plugins = match plugins_value {
    Some(Json::Array(values)) =>
      Some(
        values
        .iter()
        .filter_map(value => {
          Some(
            (@json.from_json(value, path=plugins_path) : MarketplacePluginWire),
          ) catch {
            _ => None
          }
        })
        .collect(),
      )
    _ => None
  }
  { wire_name, wire_plugins }
}

///|
fn plugin_source_from_wire(
  plugin : MarketplacePluginWire,
  kind : MarketplaceKind,
) -> String? {
  let source = match (kind, plugin.wire_source) {
    (Codex, Some(source)) => source.wire_path
    (Codex, None) => None
    (_, Some(source)) => source.wire_string
    (_, None) => None
  }
  normalize_plugin_source(source)
}

///|
fn normalized_plugins_from_json(
  text : String,
  kind : MarketplaceKind,
) -> Array[NormalizedPlugin] {
  let document = @json.parse(text) catch { _ => return [] }
  let marketplace : MarketplaceWire = @json.from_json(document) catch {
    _ => return []
  }
  match marketplace.wire_plugins {
    Some(plugins) =>
      plugins
      .iter()
      .filter_map(plugin => normalized_plugin_from_wire(plugin, kind))
      .collect()
    None => []
  }
}

///|
fn normalized_plugin_from_wire(
  plugin : MarketplacePluginWire,
  kind : MarketplaceKind,
) -> NormalizedPlugin? {
  let name = plugin.wire_name.unwrap_or("plugin")
  let source = plugin_source_from_wire(plugin, kind).unwrap_or(name)
  if is_relative_plugin_path(source) {
    Some({ name, source, description: plugin.wire_description })
  } else {
    None
  }
}

///|
fn marketplace_name_from_json(text : String) -> String {
  let document = @json.parse(text) catch { _ => return "marketplace" }
  let marketplace : MarketplaceWire = @json.from_json(document) catch {
    _ => return "marketplace"
  }
  marketplace.wire_name.unwrap_or("marketplace")
}

///|
async fn resolve_skills(
  repo_dir : String,
  kind : MarketplaceKind,
) -> Array[ResolvedSkill] {
  let path = @path.Path::join(repo_dir, marketplace_path(kind)).to_string()
  let text = @fs.read_file(path).text()
  resolve_skills_from_plugins(
    repo_dir,
    normalized_plugins_from_json(text, kind),
  )
}

///|
async fn resolve_skills_from_plugins(
  repo_dir : String,
  plugins : Array[NormalizedPlugin],
) -> Array[ResolvedSkill] {
  @async.all(
    plugins.map(plugin => () => resolve_plugin_skills(repo_dir, plugin)),
  )
  .iter()
  .flat_map(skills => skills.iter())
  .collect()
}

///|
async fn resolve_plugin_skills(
  repo_dir : String,
  plugin : NormalizedPlugin,
) -> Array[ResolvedSkill] {
  let skill_dir = (repo_dir
  |> @path.Path::join(plugin.source)
  |> @path.Path::join("skills")).to_string()
  let entries = @fs.readdir(
    skill_dir,
    include_hidden=false,
    include_special=false,
    sort=true,
  ) catch {
    _ => []
  }
  resolve_skill_entries(plugin, skill_dir, entries)
}

///|
async fn resolve_skill_entries(
  plugin : NormalizedPlugin,
  skill_dir : String,
  entries : Array[String],
) -> Array[ResolvedSkill] {
  @async.all(
    entries.map(entry => {
      () => {
        let skill_path = @path.Path::join(skill_dir, entry).to_string()
        if @fs.exists(@path.Path::join(skill_path, "SKILL.md").to_string()) {
          Some({
            plugin_name: plugin.name,
            plugin_path: plugin.source,
            skill_name: entry,
            skill_path,
          })
        } else {
          None
        }
      }
    }),
  )
  .iter()
  .filter_map(skill => skill)
  .collect()
}