#!/usr/bin/env bash
set -euo pipefail

# Link the INSTALLED canonical skills into runtime skill directories.
#
# The canonical skills ship inside the framework package itself
# (packages/mosaic/framework/skills in the monorepo) and are installed into
# $MOSAIC_HOME/skills by the framework installer — there is no second
# repository to clone. This script only maintains the runtime symlinks.

MOSAIC_HOME="${MOSAIC_HOME:-$HOME/.config/mosaic}"
MOSAIC_SKILLS_DIR="$MOSAIC_HOME/skills"
MOSAIC_LOCAL_SKILLS_DIR="$MOSAIC_HOME/skills-local"

# Colon-separated list of skill names to install.  When set, only these skills
# are linked into runtime skill directories.  Empty/unset = link all skills
# (the legacy "mosaic sync" full-catalog behavior).
MOSAIC_INSTALL_SKILLS="${MOSAIC_INSTALL_SKILLS:-}"

usage() {
  cat <<USAGE
Usage: $(basename "$0") [options]

Link installed skills from ~/.config/mosaic/{skills,skills-local} into runtime
skill directories. Canonical skills arrive with the framework installer; this
script never clones or pulls a second repository.

Options:
  --link-only     Accepted for compatibility; linking is now the whole job
  --no-link       Do nothing (kept for compatibility)
  -h, --help      Show help

Env:
  MOSAIC_HOME             Default: ~/.config/mosaic
  MOSAIC_INSTALL_SKILLS   Colon-separated list of skills to link (default: all)
USAGE
}

while [[ $# -gt 0 ]]; do
  case "$1" in
    --link-only)
      # Compat no-op: fetching a skills repo no longer exists.
      shift
      ;;
    --no-link)
      echo "[mosaic-skills] Nothing to do (--no-link; canonical skills ship with the installer)"
      exit 0
      ;;
    -h|--help)
      usage
      exit 0
      ;;
    *)
      echo "Unknown argument: $1" >&2
      usage >&2
      exit 1
      ;;
  esac
done

mkdir -p "$MOSAIC_HOME" "$MOSAIC_SKILLS_DIR" "$MOSAIC_LOCAL_SKILLS_DIR"

if [[ ! -d "$MOSAIC_SKILLS_DIR" ]]; then
  echo "[mosaic-skills] Canonical skills dir missing: $MOSAIC_SKILLS_DIR" >&2
  echo "[mosaic-skills] Canonical skills ship with the framework — reinstall or update the framework package" >&2
  exit 1
fi

# Skills are linked into the MOSAIC-OWNED harness homes, never a base install.
# Paths mirror the config-dir env vars the launcher injects (HARNESS_HOME_ENV in
# commands/launch.js):
#   claude   CLAUDE_CONFIG_DIR      -> <home>/skills
#   pi       PI_CODING_AGENT_DIR    -> <home>/skills   (replaces ~/.pi/agent)
#   codex    CODEX_HOME             -> <home>/skills
#   opencode XDG_CONFIG_HOME        -> <home>/opencode/skills  (XDG adds a level)
link_targets=(
  "$MOSAIC_HOME/.claude/skills"
  "$MOSAIC_HOME/.codex/skills"
  "$MOSAIC_HOME/.opencode/opencode/skills"
  "$MOSAIC_HOME/.pi/skills"
)

# Pre-isolation installs planted the same symlink farm directly in the operator's
# base installs. Those are now orphaned: the launcher no longer reads them, but
# they persist and make a "clean" base install look mosaic-managed.
legacy_link_targets=(
  "$HOME/.claude/skills"
  "$HOME/.codex/skills"
  "$HOME/.config/opencode/skills"
  "$HOME/.pi/agent/skills"
)

canonical_real="$(readlink -f "$MOSAIC_SKILLS_DIR")"
local_real="$(readlink -f "$MOSAIC_LOCAL_SKILLS_DIR")"

# Build an associative array from the colon-separated whitelist for O(1) lookup.
# When MOSAIC_INSTALL_SKILLS is empty, all skills are allowed.
declare -A _skill_whitelist=()
_whitelist_active=0
if [[ -n "$MOSAIC_INSTALL_SKILLS" ]]; then
  _whitelist_active=1
  IFS=':' read -ra _wl_items <<< "$MOSAIC_INSTALL_SKILLS"
  for _item in "${_wl_items[@]}"; do
    [[ -n "$_item" ]] && _skill_whitelist["$_item"]=1
  done
fi

is_skill_selected() {
  local name="$1"
  if [[ $_whitelist_active -eq 0 ]]; then
    return 0
  fi
  [[ -n "${_skill_whitelist[$name]:-}" ]] && return 0
  return 1
}

link_skill_into_target() {
  local skill_path="$1"
  local target_dir="$2"

  local name link_path
  name="$(basename "$skill_path")"

  # Do not distribute hidden/system skill directories globally.
  if [[ "$name" == .* ]]; then
    return
  fi

  # Respect the install whitelist (set during first-run wizard).
  if ! is_skill_selected "$name"; then
    return
  fi

  link_path="$target_dir/$name"

  if [[ -L "$link_path" ]]; then
    local raw_target resolved_target
    raw_target="$(readlink "$link_path")"
    resolved_target="$(node -e 'const p=require("node:path"); process.stdout.write(p.resolve(p.dirname(process.argv[1]), process.argv[2]));' "$link_path" "$raw_target")"
    if [[ "$resolved_target" == "$canonical_real/"* || "$resolved_target" == "$local_real/"* ]]; then
      ln -sfn "$skill_path" "$link_path"
    else
      echo "[mosaic-skills] Preserve foreign runtime symlink: $link_path"
    fi
    return
  fi

  if [[ -e "$link_path" ]]; then
    echo "[mosaic-skills] Preserve existing runtime-specific entry: $link_path"
    return
  fi

  ln -s "$skill_path" "$link_path"
}

is_mosaic_skill_name() {
  local name="$1"
  # -d follows symlinks; -L catches broken symlinks that still indicate ownership
  [[ -d "$MOSAIC_SKILLS_DIR/$name" || -L "$MOSAIC_SKILLS_DIR/$name" ]] && return 0
  [[ -d "$MOSAIC_LOCAL_SKILLS_DIR/$name" || -L "$MOSAIC_LOCAL_SKILLS_DIR/$name" ]] && return 0
  return 1
}

prune_stale_links_in_target() {
  local target_dir="$1"

  while IFS= read -r -d '' link_path; do
    local name resolved
    name="$(basename "$link_path")"

    if is_mosaic_skill_name "$name"; then
      continue
    fi

    # -m resolves lexical dangling targets too. If resolution fails, ownership
    # is unproven and the link must be preserved.
    resolved="$(readlink -m "$link_path" 2>/dev/null || true)"
    # $canonical_real must be length-checked BEFORE use as a prefix: if it were
    # ever empty, "$resolved" == "$canonical_real/"* collapses to == "/"* and
    # matches every absolute path. Combined with the is_mosaic_skill_name skip
    # above, that inverts the function precisely — it would delete exactly the
    # FOREIGN symlinks and keep the mosaic ones. (#1087, reported by mos-claude.)
    if [[ -n "$resolved" && -n "$canonical_real" && "$resolved" == "$canonical_real/"* ]]; then
      rm -f "$link_path"
      echo "[mosaic-skills] Removed stale retired skill link: $link_path"
    fi
  done < <(find "$target_dir" -mindepth 1 -maxdepth 1 -type l -print0)
}

# Remove mosaic-owned symlinks left in a base install by a pre-isolation sync.
#
# Ownership is proven by RESOLUTION, not by name: only links resolving inside the
# canonical or local skills dirs are removed. Anything else — a real directory, a
# link elsewhere, an unresolvable link — is left untouched. This mirrors the
# refusal in commands/skill.js ("only symlinks pointing inside the Mosaic skills
# directory are managed") and preserves e.g. codex's own `.system` dir.
#
# The directory itself is kept: mosaic-doctor warns when ~/.pi/agent/skills is
# missing, and an empty dir is the correct end state, not an absent one.
cleanup_legacy_target() {
  local target_dir="$1"
  local removed=0 kept=0

  [[ -d "$target_dir" ]] || return 0

  while IFS= read -r -d '' link_path; do
    local resolved owned=0
    resolved="$(readlink -m "$link_path" 2>/dev/null || true)"

    # Guard the empty-prefix trap: an unset *_real would make "$resolved" == "/"*
    # match every absolute path and delete foreign links.
    if [[ -n "$resolved" ]]; then
      if [[ -n "$canonical_real" && "$resolved" == "$canonical_real/"* ]]; then
        owned=1
      elif [[ -n "$local_real" && "$resolved" == "$local_real/"* ]]; then
        owned=1
      fi
    fi

    if [[ $owned -eq 1 ]]; then
      rm -f "$link_path"
      removed=$((removed + 1))
    else
      kept=$((kept + 1))
    fi
  done < <(find "$target_dir" -mindepth 1 -maxdepth 1 -type l -print0)

  if [[ $removed -gt 0 ]]; then
    echo "[mosaic-skills] Legacy cleanup: removed $removed mosaic symlink(s) from $target_dir (preserved $kept foreign)"
  fi
}

for legacy in "${legacy_link_targets[@]}"; do
  # Skip anything that is also a current target, so isolation can never
  # self-destruct if the two lists ever overlap.
  skip=0
  for target in "${link_targets[@]}"; do
    [[ "$legacy" == "$target" ]] && skip=1
  done
  [[ $skip -eq 1 ]] && continue
  cleanup_legacy_target "$legacy"
done

for target in "${link_targets[@]}"; do
  mkdir -p "$target"

  # If target already resolves to canonical dir, skip to avoid self-link recursion/corruption.
  target_real="$(readlink -f "$target" 2>/dev/null || true)"
  if [[ -n "$target_real" && "$target_real" == "$canonical_real" ]]; then
    echo "[mosaic-skills] Skip target (already canonical): $target"
    continue
  fi

  prune_stale_links_in_target "$target"

  while IFS= read -r -d '' skill; do
    link_skill_into_target "$skill" "$target"
  done < <(find "$MOSAIC_SKILLS_DIR" -mindepth 1 -maxdepth 1 -type d -print0)

  if [[ -d "$MOSAIC_LOCAL_SKILLS_DIR" ]]; then
    while IFS= read -r -d '' skill; do
      link_skill_into_target "$skill" "$target"
    done < <(find "$MOSAIC_LOCAL_SKILLS_DIR" -mindepth 1 -maxdepth 1 \( -type d -o -type l \) -print0)
  fi

  echo "[mosaic-skills] Linked skills into: $target"
done

echo "[mosaic-skills] Complete"
