#!/bin/sh
set -eu

usage() {
    cat <<'EOF'
usage: ink-skills list
       ink-skills install [--user | --project DIR] [SKILL ...]
       ink-skills add [--user | --project DIR] [--ref REF] REPOSITORY [PATH ...]

Install Ink skills as symlinks from this checkout or a pinned Git artifact.

Commands:
  list             List skills in this checkout as TSV.
  install          Link named local skills; with no names, link every skill.
  add              Fetch REPOSITORY, pin REF to a commit, materialize an
                   immutable snapshot, and link skill PATHs from it. PATH
                   defaults to every skills/*/SKILL.md directory.

Targets:
  --user           $INK_SKILLS_HOME or $HOME/.ink/skills (default)
  --project DIR    DIR/.ink/skills

Git storage:
  $INK_SKILLS_STORE or $XDG_DATA_HOME/ink-skills, otherwise
  $HOME/.local/share/ink-skills. Git, tar, and sha256sum are required by `add`.
  Authentication belongs to Git's SSH agent or credential helper; credentialed
  HTTP URLs are refused. Installed provenance is written to
  TARGET/.ink-skills.tsv without modifying SKILL.md.

Output:
  TSV with SKILL, TARGET, ACTION, SOURCE, COMMIT, and SHA256 columns.

Exit status:
  0 success; 2 usage error; 3 collision, invalid source, or fetch failure.

Examples:
  ink-skills list
  ink-skills install audit-ink-cli configure-ink-agent
  ink-skills install --project . create-ink-tool
  ink-skills add --ref main git@git.example:team/skills.git
  ink-skills add https://git.example/team/skills.git skills/review-sql
EOF
}

die() {
    printf '%s\n' "ink-skills: $*" >&2
    exit 3
}

reject_record_breaks() {
    case $1 in
        *"	"*|*"
"*) die "tabs and newlines are not allowed: $1" ;;
    esac
}

script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd -P)
repo_dir=$(CDPATH= cd -- "$script_dir/.." && pwd -P)
skills_dir=$repo_dir/skills

list_skills() {
    printf 'SKILL\tSOURCE\n'
    for path in "$skills_dir"/*; do
        [ -d "$path" ] || continue
        [ -f "$path/SKILL.md" ] || continue
        printf '%s\t%s\n' "$(basename -- "$path")" "$path"
    done
}

select_target() {
    case $target_mode in
        user)
            if [ -n "${INK_SKILLS_HOME:-}" ]; then
                target=$INK_SKILLS_HOME
            else
                [ -n "${HOME:-}" ] || die 'HOME is unset; set HOME or INK_SKILLS_HOME'
                target=$HOME/.ink/skills
            fi
            ;;
        project)
            project=$(CDPATH= cd -- "$target_arg" 2>/dev/null && pwd -P) || die "project directory not found: $target_arg"
            target=$project/.ink/skills
            ;;
    esac
    mkdir -p -- "$target"
}

parse_target_option() {
    case $1 in
        --user)
            target_mode=user
            shift_count=1
            ;;
        --project)
            [ "$#" -ge 2 ] || {
                usage >&2
                exit 2
            }
            target_mode=project
            target_arg=$2
            shift_count=2
            ;;
        *) shift_count=0 ;;
    esac
}

frontmatter_name() {
    sed -n 's/^name:[[:space:]]*//p' "$1/SKILL.md" | sed -n '1p'
}

validate_skill_dir() {
    source_path=$1
    [ -d "$source_path" ] && [ -f "$source_path/SKILL.md" ] || die "not a skill directory: $source_path"
    [ ! -L "$source_path" ] && [ ! -L "$source_path/SKILL.md" ] || die "skill root and SKILL.md must not be symlinks: $source_path"
    skill_name=$(frontmatter_name "$source_path")
    [ -n "$skill_name" ] || die "missing frontmatter name: $source_path/SKILL.md"
    [ "$skill_name" = "$(basename -- "$source_path")" ] || die "frontmatter name does not match directory: $source_path"
    case $skill_name in
        ''|.*|*/*) die "invalid skill name: $skill_name" ;;
    esac
}

link_skill() {
    skill_name=$1
    source_path=$2
    source_label=$3
    commit=$4
    digest=$5
    destination=$target/$skill_name
    if [ -L "$destination" ]; then
        linked=$(readlink "$destination")
        [ "$linked" = "$source_path" ] || die "refusing foreign symlink: $destination -> $linked"
        action=unchanged
    elif [ -e "$destination" ]; then
        die "refusing existing path: $destination"
    else
        ln -s -- "$source_path" "$destination"
        action=linked
    fi
    printf '%s\t%s\t%s\t%s\t%s\t%s\n' "$skill_name" "$destination" "$action" "$source_label" "$commit" "$digest"
}

check_source_record() {
    skill_name=$1
    source_label=$2
    ref=$3
    commit=$4
    skill_path=$5
    digest=$6
    manifest=$target/.ink-skills.tsv
    record_needed=yes
    [ -e "$manifest" ] || return 0
    if awk -F '\t' -v skill="$skill_name" 'NR > 1 && $1 == skill { found = 1 } END { exit !found }' "$manifest"; then
        existing=$(awk -F '\t' -v skill="$skill_name" 'NR > 1 && $1 == skill { print $0; exit }' "$manifest")
        wanted=$(printf '%s\t%s\t%s\t%s\t%s\t%s' "$skill_name" "$source_label" "$ref" "$commit" "$skill_path" "$digest")
        [ "$existing" = "$wanted" ] || die "provenance collision for installed skill: $skill_name"
        record_needed=no
    fi
}

append_source_record() {
    [ "$record_needed" = yes ] || return 0
    manifest=$target/.ink-skills.tsv
    if [ ! -e "$manifest" ]; then
        printf 'SKILL\tSOURCE\tREF\tCOMMIT\tPATH\tSHA256\n' >"$manifest"
    fi
    printf '%s\t%s\t%s\t%s\t%s\t%s\n' "$skill_name" "$repository" "$ref" "$commit" "$skill_path" "$digest" >>"$manifest"
}

[ "$#" -gt 0 ] || {
    usage >&2
    exit 2
}
command=$1
shift
case $command in
    -h|--help|help)
        usage
        exit 0
        ;;
    list)
        [ "$#" -eq 0 ] || {
            usage >&2
            exit 2
        }
        list_skills
        exit 0
        ;;
    install|add) ;;
    *)
        usage >&2
        exit 2
        ;;
esac

target_mode=user
target_arg=
ref=HEAD
while [ "$#" -gt 0 ]; do
    parse_target_option "$@"
    if [ "$shift_count" -gt 0 ]; then
        shift "$shift_count"
        continue
    fi
    case $1 in
        --ref)
            [ "$command" = add ] && [ "$#" -ge 2 ] || {
                usage >&2
                exit 2
            }
            ref=$2
            shift 2
            ;;
        --)
            shift
            break
            ;;
        -*)
            usage >&2
            exit 2
            ;;
        *) break ;;
    esac
done
select_target

printf 'SKILL\tTARGET\tACTION\tSOURCE\tCOMMIT\tSHA256\n'

if [ "$command" = install ]; then
    if [ "$#" -eq 0 ]; then
        set --
        for path in "$skills_dir"/*; do
            [ -d "$path" ] || continue
            [ -f "$path/SKILL.md" ] || continue
            set -- "$@" "$(basename -- "$path")"
        done
    fi
    for skill do
        case $skill in
            ''|.*|*/*) die "invalid skill name: $skill" ;;
        esac
        source_path=$skills_dir/$skill
        validate_skill_dir "$source_path"
        link_skill "$skill_name" "$source_path" local - -
    done
    exit 0
fi

[ "$#" -gt 0 ] || {
    usage >&2
    exit 2
}
repository=$1
shift
reject_record_breaks "$repository"
reject_record_breaks "$ref"
case $ref in
    -*) die "invalid ref: $ref" ;;
esac
case $repository in
    http://*@*|https://*@*) die 'credentialed HTTP URLs are refused; use an SSH agent or Git credential helper' ;;
esac
command -v git >/dev/null 2>&1 || die 'git is required by add'
command -v tar >/dev/null 2>&1 || die 'tar is required by add'
command -v sha256sum >/dev/null 2>&1 || die 'sha256sum is required by add'

if [ -n "${INK_SKILLS_STORE:-}" ]; then
    store=$INK_SKILLS_STORE
elif [ -n "${XDG_DATA_HOME:-}" ]; then
    store=$XDG_DATA_HOME/ink-skills
else
    [ -n "${HOME:-}" ] || die 'HOME is unset; set HOME, XDG_DATA_HOME, or INK_SKILLS_STORE'
    store=$HOME/.local/share/ink-skills
fi
repo_key=$(printf '%s' "$repository" | git hash-object --stdin) || die 'cannot hash repository identity'
mirror=$store/git/$repo_key.git
mkdir -p -- "$store/git" "$store/artifacts/$repo_key"
if [ ! -d "$mirror" ]; then
    git clone --quiet --mirror -- "$repository" "$mirror" || die "cannot clone repository: $repository"
fi
git --git-dir="$mirror" fetch --quiet --force origin "$ref" || die "cannot fetch ref: $ref"
commit=$(git --git-dir="$mirror" rev-parse --verify 'FETCH_HEAD^{commit}') || die "ref does not resolve to a commit: $ref"
artifact=$store/artifacts/$repo_key/$commit
if [ ! -d "$artifact" ]; then
    temporary=$artifact.tmp.$$
    rm -rf -- "$temporary"
    mkdir -p -- "$temporary"
    if ! git --git-dir="$mirror" archive "$commit" | tar -x -C "$temporary"; then
        rm -rf -- "$temporary"
        die "cannot materialize commit: $commit"
    fi
    mv -- "$temporary" "$artifact"
fi

if [ "$#" -eq 0 ]; then
    set --
    for source_path in "$artifact"/skills/*; do
        [ -d "$source_path" ] || continue
        [ -f "$source_path/SKILL.md" ] || continue
        set -- "$@" "skills/$(basename -- "$source_path")"
    done
    [ "$#" -gt 0 ] || die "repository has no skills/*/SKILL.md at $commit"
fi

for skill_path do
    reject_record_breaks "$skill_path"
    case $skill_path in
        ''|/*|*:*|..|../*|*/../*|*/..) die "invalid repository skill path: $skill_path" ;;
    esac
    source_path=$artifact/$skill_path
    validate_skill_dir "$source_path"
    if find "$source_path" -type l -print | grep . >/dev/null 2>&1; then
        die "remote skill contains symlinks: $skill_path"
    fi
    digest=$(git --git-dir="$mirror" archive "$commit:$skill_path" | sha256sum | awk '{print $1}') || die "cannot hash skill artifact: $skill_path"
    check_source_record "$skill_name" "$repository" "$ref" "$commit" "$skill_path" "$digest"
    link_skill "$skill_name" "$source_path" "$repository" "$commit" "$digest"
    append_source_record
done
