///|
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
  }
}

///|
fn plugin_source_from_json(plugin : Json, kind : MarketplaceKind) -> String? {
  match kind {
    Codex =>
      normalize_plugin_source(
        @lens.object("source").string("path").optional().get(plugin) catch {
          _ => None
        },
      )
    _ =>
      normalize_plugin_source(
        @lens.root().string("source").optional().get(plugin) catch {
          _ => None
        },
      )
  }
}

///|
fn normalized_plugins_from_json(
  text : String,
  kind : MarketplaceKind,
) -> Array[NormalizedPlugin] {
  let document = @json.parse(text) catch { _ => return [] }
  let plugins = @lens.root().json("plugins").array().get(document) catch {
      _ => []
    }
  plugins
  .iter()
  .filter_map(plugin => normalized_plugin_from_json(plugin, kind))
  .collect()
}

///|
fn normalized_plugin_from_json(
  plugin : Json,
  kind : MarketplaceKind,
) -> NormalizedPlugin? {
  let _ = @lens.root().get(plugin) catch { _ => return None }
  let name = @lens.root().string("name").get(plugin) catch { _ => "plugin" }
  let source = plugin_source_from_json(plugin, kind).unwrap_or(name)
  if is_relative_plugin_path(source) {
    Some({
      name,
      source,
      description: @lens.root().string("description").optional().get(plugin) catch {
        _ => None
      },
    })
  } else {
    None
  }
}

///|
fn marketplace_name_from_json(text : String) -> String {
  let document = @json.parse(text) catch { _ => return "marketplace" }
  @lens.root().string("name").get(document) catch {
    _ => "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()
}