#!/bin/sh
set -eu

usage() {
    cat <<'EOF'
usage: ink-skills list
       ink-skills link [--user | --project DIR] [SKILL_DIR ...]
       ink-skills import [--user | --project DIR] sha256:HASH ARCHIVE [PATH ...]

Link local Ink skills or import a verified skill archive.

Commands:
  list      List skills shipped by this checkout as TSV.
  link      Symlink local skill directories. With no directories, link every
            skill shipped by this checkout.
  import    Verify ARCHIVE against the required SHA-256, safely materialize its
            immutable tree, then link selected skill PATHs. PATH defaults to
            every skills/*/SKILL.md directory in the archive.

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

Artifact store:
  $INK_SKILLS_STORE or $XDG_DATA_HOME/ink-skills, otherwise
  $HOME/.local/share/ink-skills.

Transport is deliberately external:
  curl -fLo skills.tar URL
  ink-skills import sha256:HASH skills.tar

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

Exit status:
  0 success; 2 usage error; 3 invalid skill, collision, digest mismatch, or
  unsafe archive.

Requirements:
  POSIX sh and standard text tools. `import` additionally needs tar and one of
  sha256sum, shasum, or openssl.
EOF
}

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

script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd -P)
repo_dir=$(CDPATH= cd -- "$script_dir/.." && pwd -P)
bundled_dir=$repo_dir/skills
cleanup_root=
cleanup_artifact=
cleanup_lock=
cleanup_all() {
    if [ -n "$cleanup_artifact" ]; then
        chmod -R u+w "$cleanup_artifact" 2>/dev/null || :
        rm -rf "$cleanup_artifact"
    fi
    [ -z "$cleanup_lock" ] || rmdir "$cleanup_lock" 2>/dev/null || :
    [ -z "$cleanup_root" ] || rm -rf "$cleanup_root"
}
trap cleanup_all EXIT HUP INT TERM

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

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

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

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_options() {
    while [ "$#" -gt 0 ]; do
        case $1 in
            --user)
                target_mode=user
                shift
                ;;
            --project)
                [ "$#" -ge 2 ] || {
                    usage >&2
                    exit 2
                }
                target_mode=project
                target_arg=$2
                shift 2
                ;;
            --)
                shift
                break
                ;;
            -*)
                usage >&2
                exit 2
                ;;
            *) break ;;
        esac
    done
    remaining_count=$#
    remaining_file=$work_args
    : >"$remaining_file"
    for arg do
        reject_record_breaks "$arg"
        printf '%s\n' "$arg" >>"$remaining_file"
    done
}

link_skill() {
    name=$1
    source_path=$2
    source_label=$3
    digest=$4
    destination=$target/$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\n' "$name" "$destination" "$action" "$source_label" "$digest"
}

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

manifest_append() {
    [ "$record_needed" = yes ] || return 0
    manifest=$target/.ink-skills.tsv
    [ -e "$manifest" ] || printf 'SKILL\tSHA256\tPATH\n' >"$manifest"
    printf '%s\t%s\t%s\n' "$name" "$digest" "$archive_path" >>"$manifest"
}

sha256_file() {
    file=$1
    if command -v sha256sum >/dev/null 2>&1; then
        sha256sum "$file" | awk '{print $1}'
    elif command -v shasum >/dev/null 2>&1; then
        shasum -a 256 "$file" | awk '{print $1}'
    elif command -v openssl >/dev/null 2>&1; then
        openssl dgst -sha256 "$file" | sed 's/^.*= //'
    else
        die 'import requires sha256sum, shasum, or openssl'
    fi
}

validate_archive_listing() {
    archive=$1
    names=$2
    types=$3
    tar -tf "$archive" >"$names" || die "cannot list archive: $archive"
    [ -s "$names" ] || die 'archive is empty'
    while IFS= read -r member; do
        reject_record_breaks "$member"
        case $member in
            ''|/*|..|../*|*/../*|*/..) die "unsafe archive path: $member" ;;
        esac
    done <"$names"
    LC_ALL=C tar -tvf "$archive" >"$types" || die "cannot inspect archive: $archive"
    awk 'substr($1, 1, 1) != "-" && substr($1, 1, 1) != "d" { exit 1 }' "$types" || die 'archive may contain only regular files and directories'
}

[ "$#" -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
        }
        printf 'SKILL\tSOURCE\n'
        for path in "$bundled_dir"/*; do
            [ -d "$path" ] && [ -f "$path/SKILL.md" ] || continue
            printf '%s\t%s\n' "$(basename -- "$path")" "$path"
        done
        exit 0
        ;;
    link|import) ;;
    *)
        usage >&2
        exit 2
        ;;
esac

target_mode=user
target_arg=
work_root=${TMPDIR:-/tmp}/ink-skills-args-$$
(umask 077 && mkdir "$work_root") || die "cannot create temporary directory: $work_root"
cleanup_root=$work_root
work_args=$work_root/args
parse_target_options "$@"
set --
while IFS= read -r arg; do set -- "$@" "$arg"; done <"$work_args"
select_target

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

if [ "$command" = link ]; then
    if [ "$#" -eq 0 ]; then
        set --
        for path in "$bundled_dir"/*; do
            [ -d "$path" ] && [ -f "$path/SKILL.md" ] || continue
            set -- "$@" "$path"
        done
    fi
    for source_path do
        case $source_path in
            /*) ;;
            *) source_path=$(CDPATH= cd -- "$(dirname -- "$source_path")" 2>/dev/null && printf '%s/%s\n' "$PWD" "$(basename -- "$source_path")") || die "skill directory not found: $source_path" ;;
        esac
        validate_skill_dir "$source_path"
        link_skill "$skill_name" "$source_path" local -
    done
    exit 0
fi

[ "$#" -ge 2 ] || {
    usage >&2
    exit 2
}
digest_spec=$1
archive=$2
shift 2
case $digest_spec in
    sha256:*) digest=${digest_spec#sha256:} ;;
    *) die 'digest must use sha256:HASH' ;;
esac
digest=$(printf '%s' "$digest" | tr 'A-F' 'a-f')
case $digest in
    *[!0-9a-f]*|'') die 'SHA-256 must contain 64 hexadecimal characters' ;;
esac
[ "${#digest}" -eq 64 ] || die 'SHA-256 must contain 64 hexadecimal characters'
[ -f "$archive" ] || die "archive not found: $archive"
command -v tar >/dev/null 2>&1 || die 'import requires tar'
actual=$(sha256_file "$archive")
actual=$(printf '%s' "$actual" | tr 'A-F' 'a-f')
[ "$actual" = "$digest" ] || die "SHA-256 mismatch: expected $digest, got $actual"

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
artifact=$store/sha256/$digest
tree=$artifact/tree
if [ -d "$artifact" ]; then
    [ -f "$artifact/complete" ] && [ "$(cat "$artifact/complete")" = "$digest" ] || die "incomplete artifact store entry: $artifact"
else
    mkdir -p -- "$store/sha256"
    lock=$store/sha256/.$digest.lock
    mkdir "$lock" 2>/dev/null || die "artifact import already in progress: $digest"
    temporary=$store/sha256/.$digest.tmp.$$
    cleanup_lock=$lock
    cleanup_artifact=$temporary
    mkdir -p -- "$temporary/tree"
    validate_archive_listing "$archive" "$work_root/names" "$work_root/types"
    tar -xf "$archive" -C "$temporary/tree" || die "cannot extract archive: $archive"
    if find "$temporary/tree" -type l -print | grep . >/dev/null 2>&1; then
        die 'archive extracted symlinks'
    fi
    if find "$temporary/tree" ! -type d ! -type f -print | grep . >/dev/null 2>&1; then
        die 'archive extracted non-file entries'
    fi
    printf '%s\n' "$digest" >"$temporary/complete"
    chmod -R a-w "$temporary"
    mv "$temporary" "$artifact"
    cleanup_artifact=
    rmdir "$lock"
    cleanup_lock=
fi

if [ "$#" -eq 0 ]; then
    set --
    for skill_dir in "$tree"/skills/*; do
        [ -d "$skill_dir" ] && [ -f "$skill_dir/SKILL.md" ] || continue
        set -- "$@" "skills/$(basename -- "$skill_dir")"
    done
    [ "$#" -gt 0 ] || die 'archive has no skills/*/SKILL.md directories'
fi

for archive_path do
    case $archive_path in
        ''|/*|*:*|..|../*|*/../*|*/..) die "invalid archive skill path: $archive_path" ;;
    esac
    source_path=$tree/$archive_path
    validate_skill_dir "$source_path"
    name=$skill_name
    manifest_check "$name" "$digest" "$archive_path"
    link_skill "$name" "$source_path" artifact "$digest"
    manifest_append
done
