#!/bin/sh -u
# spc - system package control
# (package manager)
# codename: convey

readonly PROG='spc'
readonly VERSION='1.2.0'

: "${SPC_ROOT:=/}"
: "${DTR_CACHE:=/var/cache/dtr}"
: "${SPC_TARGET:=}"
: "${SPC_LIBC:=}"
: "${SPC_SYSROOT:=}"
: "${SPC_REPO:=https://pkg.derivelinux.org/pkg}"
: "${SPC_VERBOSE:=0}"
: "${SPC_FORCE:=0}"
: "${NO_COLOR:=}"

SCRIPT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)"
: "${SPC_PORTS:=$SCRIPT_DIR/ports}"

set_spc_paths() {
    _root="${SPC_ROOT%/}"
    [ -n "$_root" ] || _root="/"
    : "${SPC_DB:=$_root/var/lib/spc/installed}"
    : "${SPC_LOCK:=$_root/var/lib/spc/lock}"
}

set_spc_paths

# === LOGGING ===
strip_tags() { printf "$@" | sed 's/<[^>]*>//g'; }

log_printf="strip_tags"
if [ -z "$NO_COLOR" ] && _cmd="$(command -v tmlf 2>/dev/null)"; then
    log_printf="$_cmd"
fi

die() { $log_printf '%s: <bg-red>error</bg-red>: %s\n' "$PROG" "$*" >&2; exit 1; }
warn() { $log_printf '%s: <bg-yellow>warning</bg-yellow>: %s\n' "$PROG" "$*" >&2; }
msg() { $log_printf '%s: %s\n' "$PROG" "$*"; }
verbose() { [ "$SPC_VERBOSE" -eq 1 ] && printf '%s\n' "$*"; }

# === LOCKFILE FORMAT ===
# Header fields (key: value)
#   name: package-name
#   version: 1.2.3
#   release: 1
#   deps: (section marker)
#
# Dependency lines (prefix syntax)
#   /pkg-name    = link-time dependency
#   >pkg-name    = runtime dependency
#   .pkg-name    = build-time dependency
#
# File entries (pipe-delimited)
#   path|perms|owner:group|type|size|hash
#
# Types: f=file d=dir l=link c=config
# Defaults: perms=-, owner=-, hash=-

parse_lock_entry() {
    IFS='|' read -r _path _perms _owner _type _size _hash <<EOF
$1
EOF
    printf '%s\n%s\n%s\n%s\n%s\n%s\n' "$_path" "$_perms" "$_owner" "$_type" "$_size" "$_hash"
}

get_lock_meta() {
    _lock="$SPC_LOCK/$1.lock"
    [ -f "$_lock" ] || return 1
    case "$2" in
        name)    grep '^name:' "$_lock" | head -n1 | cut -d: -f2- | sed 's/^ *//' ;;
        version) grep '^version:' "$_lock" | head -n1 | cut -d: -f2- | sed 's/^ *//' ;;
        release) grep '^release:' "$_lock" | head -n1 | cut -d: -f2- | sed 's/^ *//' ;;
        commit)  grep '^commit:' "$_lock" | head -n1 | cut -d: -f2- | sed 's/^ *//' ;;
    esac
}

get_lock_deps() {
    _lock="$SPC_LOCK/$1.lock"
    [ -f "$_lock" ] || return 1
    awk '/^deps:/{p=1;next}/^files:/{p=0}p&&NF{print}' "$_lock"
}

get_lock_files() {
    _lock="$SPC_LOCK/$1.lock"
    [ -f "$_lock" ] || return 1
    # Handle v2 pipe format (field 1) or legacy format (whole line)
    awk '/^files:/{p=1;next}p&&NF{split($0,a,"|"); print a[1]}' "$_lock"
}

get_lock_entries() {
    _lock="$SPC_LOCK/$1.lock"
    [ -f "$_lock" ] || return 1
    awk '/^files:/{p=1;next}p&&NF{print}' "$_lock"
}

# === DATABASE ===
is_installed() { [ -f "$SPC_DB" ] && grep -qxF "$1" "$SPC_DB"; }

mark_installed() {
    mkdir -p "$(dirname "$SPC_DB")"
    printf '%s\n' "$1" >> "$SPC_DB"
    sort -u "$SPC_DB" -o "$SPC_DB"
}

mark_removed() {
    [ -f "$SPC_DB" ] || return 0
    grep -vxF "$1" "$SPC_DB" > "$SPC_DB.tmp" && mv "$SPC_DB.tmp" "$SPC_DB"
}

# === ARCHIVE ===
parse_pkg_name() {
    _base="${1%.spc.*}" # Remove extension first (standardized)

    # Ensure we actually stripped the extension (strict mode)
    if [ "$_base" = "$1" ]; then
         die "invalid package extension (expected .spc.*)"
    fi

    _name="${_base%+*}"
    _rest="${_base#*+}"
    _version="${_rest%%-*}"
    _release="${_rest##*-}"

    if [ "$_name+$_version-$_release" != "$_base" ]; then
          die "invalid format (expected: name+version-release.spc.ext)"
    fi
    printf '%s\n%s\n%s\n' "$_name" "$_version" "$_release"
}

x_detect_compression() {
    case "$1" in
        *.spc.tgz)  echo 'z' ;;
        *.spc.tbz2) echo 'j' ;;
        *.spc.tzst) echo 'zst' ;;
        *) die "unknown archive format: $1" ;;
    esac
}

# Normalize path: remove ./ prefix, resolve /./, ensure leading /
clean_path() {
    _p="$1"
    _p="${_p#./}"
    _p="${_p#/}"
    printf '/%s' "$_p" | sed 's|/\./|/|g; s|//|/|g'
}

# === CREATE ===
x_create() {
    _dir="${1:-}"
    _out="${2:-}"

    [ -z "$_dir" ] && die "source directory required"
    [ -d "$_dir" ] || die "directory not found: $_dir"
    [ -z "$_out" ] && die "output filename required"

    # Enforce naming convention
    case "$_out" in
        *.spc.tzst) ;;
        *.spc.tgz|*.spc.tbz2) warn "recommend zstd compression (.spc.tzst) for packages" ;;
        *.spc.*) warn "unknown compression extension, assuming valid tar" ;;
        *) die "package must end in .spc.{tzst,tgz,tbz2}" ;;
    esac

    _parsed="$(parse_pkg_name "${_out##*/}")"
    _name="$(printf '%s' "$_parsed" | sed -n 1p)"
    _version="$(printf '%s' "$_parsed" | sed -n 2p)"
    _release="$(printf '%s' "$_parsed" | sed -n 3p)"

    msg "creating package $_out from $_dir"
    mkdir -p "$(dirname "$_out")"

    _comp="$(x_detect_compression "$_out")"
    if tar --version 2>/dev/null | grep -q GNU; then
        _tar_owner="--owner=0 --group=0 --numeric-owner"
    else
        _tar_owner="--numeric-owner"
    fi

    _manifest="$_dir/.manifest"
    rm -f "$_manifest"
    _commit=""

    if [ "${_version#git:}" != "$_version" ]; then
        _commit="${_version#git:}"
    elif [ -f "$_dir/.info" ]; then
        _commit="$(grep '^commit:' "$_dir/.info" | cut -d: -f2- | sed 's/^ *//')"
    fi

    {
        printf 'name: %s\n' "$_name"
        if [ "$_version" = "git" ] && [ -n "$_commit" ]; then
            _version="git:${_commit}"
        fi
        printf 'version: %s\n' "$_version"
        printf 'release: %s\n' "$_release"
        [ -n "$_commit" ] && printf 'commit: %s\n' "$_commit"
        printf 'deps:\n'
        printf 'files:\n'

        find "$_dir" -mindepth 1 -print | while IFS= read -r _abs_path; do
            _path="${_abs_path#$_dir}"
            _path="$(clean_path "$_path")"
            case "$_path" in
                /.manifest|/.info) continue ;;
            esac

            if [ -L "$_abs_path" ]; then
                _type='l'
                _perms="$(stat -c %A "$_abs_path")"
                _owner_str="$(stat -c %U:%G "$_abs_path")"
                _size='-'
                _hash='-'
            elif [ -d "$_abs_path" ]; then
                _type='d'
                _perms="$(stat -c %A "$_abs_path")"
                _owner_str="$(stat -c %U:%G "$_abs_path")"
                _size='-'
                _hash='-'
            elif [ -f "$_abs_path" ]; then
                _type='f'
                _perms="$(stat -c %A "$_abs_path")"
                _owner_str="$(stat -c %U:%G "$_abs_path")"
                _size="$(stat -c %s "$_abs_path")"
                _hash="$(b3sum "$_abs_path" 2>/dev/null | cut -d' ' -f1)"
                case "$_path" in /etc/*) _type='c' ;; esac
            else
                _type='f'
                _perms='-'
                _owner_str='-'
                _size='-'
                _hash='-'
            fi

            if [ "$_perms" = "-" ]; then
                _perms_oct='-'
            else
                _perms_oct="$(cvperms "$_perms")"
            fi
            printf '%s|%s|%s|%s|%s|%s\n' "$_path" "$_perms_oct" "$_owner_str" "$_type" "$_size" "$_hash"
        done
    } > "$_manifest"

    if [ "$_comp" = "zst" ]; then
        export ZSTD_NBTHREADS="${ZSTD_NBTHREADS:-0}"
        tar -C "$_dir" $_tar_owner -cf - .manifest --exclude=.manifest . | zstd -q -T0 -o "$_out" || die "failed to create archive"
    else
        tar -C "$_dir" $_tar_owner -c"$_comp"f "$_out" .manifest --exclude=.manifest . || die "failed to create archive"
    fi
    rm -f "$_manifest"
    msg "package created: $_out"
}

# === CVPERMS ===
cvperms() {
    # converts symbolic (e.g., drwxr-xr-x) to numeric (e.g., 755)
    local s="${1?}" p=0

    # User
    case "$s" in ?r????????) p=$((p+400));; esac
    case "$s" in ??w???????) p=$((p+200));; esac
    case "$s" in ???x??????) p=$((p+100));; ???s??????) p=$((p+4100));; ???S??????) p=$((p+4000));; esac

    # Group
    case "$s" in ????r?????) p=$((p+40));; esac
    case "$s" in ?????w????) p=$((p+20));; esac
    case "$s" in ??????x????) p=$((p+10));; ??????s????) p=$((p+2010));; ??????S????) p=$((p+2000));; esac

    # Other
    case "$s" in ???????r??) p=$((p+4));; esac
    case "$s" in ????????w?) p=$((p+2));; esac
    case "$s" in ?????????x) p=$((p+1));; ?????????t) p=$((p+1001));; ?????????T) p=$((p+1000));; esac

    echo "$p"
}

# === INSTALL ===
x_install() {
    [ -f "$1" ] || die "archive not found: $1"

    _parsed="$(parse_pkg_name "${1##*/}")"
    _name="$(printf '%s' "$_parsed" | sed -n 1p)"
    _version="$(printf '%s' "$_parsed" | sed -n 2p)"
    _release="$(printf '%s' "$_parsed" | sed -n 3p)"
    _upgrade=0

    is_installed "$_name" && { _upgrade=1; msg "upgrading $_name"; }

    _comp="$(x_detect_compression "$1")"
    if [ "$_comp" = "zst" ]; then
        zstd -q -d -c "$1" | tar -tf - >/dev/null 2>&1 || die "corrupted archive: $1"
    else
        tar -t${_comp}f "$1" >/dev/null 2>&1 || die "corrupted archive: $1"
    fi

    msg "extracting metadata"
    _filelist="/tmp/spc-files.$$"
    if [ "$_comp" = "zst" ]; then
        zstd -q -d -c "$1" | tar -tf - > "$_filelist" 2>/dev/null || die "failed to read archive"
    else
        tar -t${_comp}f "$1" > "$_filelist" 2>/dev/null || die "failed to read archive"
    fi

    _tmpdir="$(mktemp -d /tmp/spc-install.XXXXXX)" || die "failed to create temp dir"

    msg "extracting files"
    if [ "$_comp" = "zst" ]; then
        zstd -q -d -c "$1" | tar -xphf - -C "$_tmpdir" || { rm -f "$_filelist"; rm -rf "$_tmpdir"; die "extraction failed"; }
    else
        tar -x${_comp}hpf "$1" -C "$_tmpdir" || { rm -f "$_filelist"; rm -rf "$_tmpdir"; die "extraction failed"; }
    fi

    # === METADATA HANDLING ===
    # Read and remove package metadata files that shouldn't persist on the system
    _meta_commit=""
    _manifest="$_tmpdir/.manifest"
    _manifest_copy=""
    _use_manifest=0

    if [ -f "$_manifest" ]; then
        _use_manifest=1
        _manifest_copy="$(mktemp /tmp/spc-manifest.XXXXXX)" || { rm -f "$_filelist"; rm -rf "$_tmpdir"; die "failed to create manifest copy"; }
        cp "$_manifest" "$_manifest_copy" || { rm -f "$_filelist" "$_manifest_copy"; rm -rf "$_tmpdir"; die "failed to copy manifest"; }

        _b3sum_cmd="b3sum"
        if ! command -v b3sum >/dev/null 2>&1; then
            if [ -x "$_tmpdir/usr/bin/b3sum" ]; then
                _b3sum_cmd="$_tmpdir/usr/bin/b3sum"
            else
                rm -f "$_filelist" "$_manifest_copy"
                rm -rf "$_tmpdir"
                die "b3sum not found (required for manifest verification)"
            fi
        fi

        _mlist="$(mktemp /tmp/spc-manifest-list.XXXXXX)" || { rm -f "$_filelist" "$_manifest_copy"; rm -rf "$_tmpdir"; die "failed to create manifest list"; }
        awk '/^files:/{p=1;next}p&&NF{print}' "$_manifest_copy" >"$_mlist"
        _bad=0
        while IFS= read -r _entry; do
            [ -z "$_entry" ] && continue
            _parsed="$(parse_lock_entry "$_entry")"
            _path="$(echo "$_parsed" | sed -n 1p)"
            _type="$(echo "$_parsed" | sed -n 4p)"
            _hash="$(echo "$_parsed" | sed -n 6p)"
            case "$_type" in
                f|c)
                    [ -z "$_hash" ] && continue
                    [ "$_hash" = "-" ] && continue
                    if [ ! -f "$_tmpdir$_path" ]; then
                        _bad=1
                        _bad_msg="manifest check failed: missing $_path"
                        break
                    fi
                    _current="$($_b3sum_cmd "$_tmpdir$_path" 2>/dev/null | cut -d' ' -f1)"
                    if [ "$_current" != "$_hash" ]; then
                        _bad=1
                        _bad_msg="manifest check failed: $_path"
                        break
                    fi
                    ;;
            esac
        done <"$_mlist"
        rm -f "$_mlist"
        if [ "$_bad" -ne 0 ]; then
            rm -f "$_filelist" "$_manifest_copy"
            rm -rf "$_tmpdir"
            die "$_bad_msg"
        fi

        rm -f "$_manifest" "$_tmpdir/.info"
    else
        if [ -f "$_tmpdir/.info" ]; then
            _meta_commit="$(grep '^commit:' "$_tmpdir/.info" | cut -d: -f2- | sed 's/^ *//')"
            rm -f "$_tmpdir/.info"
        fi
    fi

    msg "hashes verified"
    tar -C "$_tmpdir" -cpf - . | tar -C "${SPC_ROOT%/}/" --keep-directory-symlink -xpf - || { rm -f "$_filelist" "$_manifest_copy"; rm -rf "$_tmpdir"; die "extraction failed"; }

    msg "generating lockfile"
    mkdir -p "$SPC_LOCK"
    _lockfile="$SPC_LOCK/$_name.lock"
    
    # Backup old lockfile if upgrading
    [ "$_upgrade" -eq 1 ] && [ -f "$_lockfile" ] && cp "$_lockfile" "$_lockfile.old"

    {
        if [ "$_use_manifest" -eq 1 ]; then
            cat "$_manifest_copy"
        else
            printf 'name: %s\n' "$_name"
            printf 'version: %s\n' "$_version"
            printf 'release: %s\n' "$_release"
            [ -n "$_meta_commit" ] && printf 'commit: %s\n' "$_meta_commit"

            printf 'deps:\n'

            printf 'files:\n'

            while IFS= read -r _raw_path; do
                [ -z "$_raw_path" ] && continue

                _path="$(clean_path "$_raw_path")"

                # Filter out metadata files from the lockfile manifest
                case "$_path" in
                    /.info|/.manifest) continue ;;
                esac

                _abs_path="${SPC_ROOT%/}$_path"

                if [ -L "$_abs_path" ]; then
                    _type='l'
                    _perms="$(stat -c %A "$_abs_path")"
                    _owner_str="$(stat -c %U:%G "$_abs_path")"
                    _size='-'
                    _hash='-'
                elif [ -d "$_abs_path" ]; then
                    _type='d'
                    _perms="$(stat -c %A "$_abs_path")"
                    _owner_str="$(stat -c %U:%G "$_abs_path")"
                    _size='-'
                    _hash='-'
                elif [ -f "$_abs_path" ]; then
                    _type='f'
                    _perms="$(stat -c %A "$_abs_path")"
                    _owner_str="$(stat -c %U:%G "$_abs_path")"
                    _size="$(stat -c %s "$_abs_path")"
                    _hash="$(b3sum "$_abs_path" 2>/dev/null | cut -d' ' -f1)"
                else
                    continue
                fi

                case "$_path" in
                    *.new)
                        _orig="${_path%.new}"
                        _orig_abs="${SPC_ROOT%/}$_orig"
                        if [ ! -e "$_orig_abs" ] || cmp -s "$_abs_path" "$_orig_abs" 2>/dev/null; then
                            mv -f "$_abs_path" "$_orig_abs" 2>/dev/null
                            _path="$_orig"
                            _abs_path="$_orig_abs"
                        fi
                        _type='c'
                        ;;
                    /etc/*)
                        [ "$_type" = 'f' ] && _type='c'
                        ;;
                esac

                _perms_oct="$(cvperms "$_perms")"
                printf '%s|%s|%s|%s|%s|%s\n' "$_path" "$_perms_oct" "$_owner_str" "$_type" "$_size" "$_hash"
                verbose "installed: $_path"
            done < "$_filelist"
        fi
    } > "$_lockfile"

    if [ "$_use_manifest" -eq 1 ]; then
        rm -f "$_manifest_copy"
    fi
    rm -rf "$_tmpdir"

    if [ "$_upgrade" -eq 1 ] && [ -f "$_lockfile.old" ]; then
        get_lock_files "$_name" < "$_lockfile.old" | while IFS= read -r _old; do
            get_lock_files "$_name" | grep -qxF "$_old" && continue
            if [ -d "${SPC_ROOT%/}$_old" ]; then
                rmdir "${SPC_ROOT%/}$_old" 2>/dev/null && verbose "removed: $_old"
            else
                rm -f "${SPC_ROOT%/}$_old" && verbose "removed: $_old"
            fi
        done
        rm -f "$_lockfile.old"
    fi

    rm -f "$_filelist"
    mark_installed "$_name"
    [ -x /sbin/ldconfig ] && /sbin/ldconfig -r "${SPC_ROOT:-/}"
    msg "installed: $_name+$_version-$_release"
}

# === REMOVE ===
x_remove() {
    is_installed "$1" || die "package '$1' not installed"
    [ -f "$SPC_LOCK/$1.lock" ] || die "lockfile missing for '$1'"

    msg "removing: $1"
    get_lock_entries "$1" | tac | while IFS= read -r _entry; do
        [ -z "$_entry" ] && continue
        _parsed="$(parse_lock_entry "$_entry")"
        _path="$(echo "$_parsed" | sed -n 1p)"
        _type="$(echo "$_parsed" | sed -n 4p)"

        case "$_type" in
            d) rmdir "${SPC_ROOT%/}$_path" 2>/dev/null && verbose "removed: $_path" ;;
            *) rm -f "${SPC_ROOT%/}$_path" && verbose "removed: $_path" ;;
        esac
    done

    rm -f "$SPC_LOCK/$1.lock"
    mark_removed "$1"
    msg "removed: $1"
}

# === QUERY ===
x_list() {
    [ -f "$SPC_DB" ] || { msg 'no packages installed'; return 0; }
    sort -u "$SPC_DB" | while IFS= read -r _p; do
        _v="$(get_lock_meta "$_p" version)"
        _r="$(get_lock_meta "$_p" release)"
        printf '%s' "$_p"
        [ -n "$_v" ] && printf '+%s' "$_v"
        [ -n "$_r" ] && printf '-%s' "$_r"
        printf '\n'
    done
}

x_files() {
    is_installed "$1" || die "package '$1' not installed"
    get_lock_files "$1"
}

x_owner() {
    _raw_arg="${1}"
    _file="$(clean_path "$_raw_arg")"
    [ -f "$SPC_DB" ] || return 1

    _candidates="/tmp/spc-owner-candidates.$$"
    {
        echo "$_file"

        _dir="$(dirname "$_file")"
        _base="$(basename "$_file")"

        # Find all symlinks in / and /usr that might alias our directory
        for _root in / /usr; do
            [ -d "$_root" ] || continue
            for _entry in "$_root"/*; do
                [ -L "$_entry" ] || continue
                _target="$(readlink "$_entry")"
                _target="$(clean_path "$_target")"
                _entry_clean="$(clean_path "$_entry")"

                # Bidirectional check
                if [ "$_dir" = "$_target" ]; then
                    echo "$_entry_clean/$_base"
                elif [ "$_dir" = "$_entry_clean" ]; then
                    echo "$_target/$_base"
                fi
            done
        done
    } | sort -u > "$_candidates"

    sort -u "$SPC_DB" | while IFS= read -r _p; do
        get_lock_files "$_p" | grep -qxF -f "$_candidates" && printf '%s\n' "$_p"
    done

    rm -f "$_candidates"
}

x_info() {
    is_installed "$1" || die "package '$1' not installed"
    [ -f "$SPC_LOCK/$1.lock" ] || die "lockfile missing for '$1'"
    _n="$(get_lock_meta "$1" name)"
    _v="$(get_lock_meta "$1" version)"
    _r="$(get_lock_meta "$1" release)"
    _c="$(get_lock_meta "$1" commit)"
    _d="$(get_lock_deps "$1" | grep -c . || echo 0)"
    _f="$(get_lock_files "$1" | grep -c . || echo 0)"
    printf 'package: %s\n' "$_n"
    [ -n "$_v" ] && printf 'version: %s\n' "$_v"
    [ -n "$_r" ] && printf 'release: %s\n' "$_r"
    [ -n "$_c" ] && printf 'commit: %.8s\n' "$_c"
    printf 'dependencies: %s\nfiles: %s\n' "$_d" "$_f"
}

x_verify() {
    is_installed "$1" || die "package '$1' not installed"
    [ -f "$SPC_LOCK/$1.lock" ] || die "lockfile missing for '$1'"

    _bad=0
    get_lock_entries "$1" | while IFS= read -r _entry; do
        [ -z "$_entry" ] && continue
        _parsed="$(parse_lock_entry "$_entry")"
        _path="$(echo "$_parsed" | sed -n 1p)"
        _type="$(echo "$_parsed" | sed -n 4p)"
        _size="$(echo "$_parsed" | sed -n 5p)"
        _hash="$(echo "$_parsed" | sed -n 6p)"

        if [ ! -e "${SPC_ROOT%/}$_path" ]; then
            printf 'missing: %s\n' "$_path"
            _bad=1
            continue
        fi

        if [ "$_type" = 'f' ] || [ "$_type" = 'c' ]; then
            if [ "$_hash" != '-' ] && [ -f "${SPC_ROOT%/}$_path" ]; then
                _current="$(b3sum "${SPC_ROOT%/}$_path" 2>/dev/null | cut -d' ' -f1)"
                [ "$_current" != "$_hash" ] && { printf 'modified: %s\n' "$_path"; _bad=1; }
            fi
            if [ "$_size" != '-' ] && [ -f "${SPC_ROOT%/}$_path" ]; then
                _current="$(stat -c %s "${SPC_ROOT%/}$_path" 2>/dev/null)"
                [ "$_current" != "$_size" ] && { printf 'size changed: %s\n' "$_path"; _bad=1; }
            fi
        fi
    done

    [ "$_bad" -eq 0 ] && msg "package '$1' verified"
    return $_bad
}

# === MIGRATION ===
# Internal helper to convert a lockfile stream to v2
_migrate_lockfile_stream() {
    local _pkg_name="$1"
    local _state="header"

    # Always start with name in v2
    echo "name: $_pkg_name"

    while IFS= read -r _line; do
        # Detect sections
        if [ "$_line" = "deps:" ]; then
            _state="deps"
            echo "deps:"
            continue
        elif [ "$_line" = "files:" ]; then
            _state="files"
            echo "files:"
            continue
        fi

        if [ "$_state" = "header" ]; then
            # Parse key: value
            case "$_line" in
                version:*) echo "version: $(echo "${_line#*:}" | xargs)" ;;
                release:*) echo "release: $(echo "${_line#*:}" | xargs)" ;;
                commit:*)  echo "commit: $(echo "${_line#*:}" | xargs)" ;;
                # Ignore Detour style comments
            esac
        elif [ "$_state" = "deps" ]; then
             [ -z "$_line" ] && continue
             # Normalize dependency prefixes if missing
             case "$_line" in
                /*|.*|\>*) echo "$_line" ;;
                *) echo "/$_line" ;; # Default to link-time if unspecified (old Detour behavior)
             esac
        elif [ "$_state" = "files" ]; then
             [ -z "$_line" ] && continue
             
             # Check if already v2 (contains pipe)
             if echo "$_line" | grep -q "|"; then
                 echo "$_line"
                 continue
             fi
             
             # It is v1/Detour (just path). We must generate metadata.
             local _fpath="$_line"
             local _abs="${SPC_ROOT%/}$_fpath"
             
             if [ -e "$_abs" ] || [ -L "$_abs" ]; then
                 local _perms _owner _type _size _hash _perms_oct
                 
                 if [ -L "$_abs" ]; then
                     _type="l"
                     _perms=$(stat -c %A "$_abs")
                     _owner=$(stat -c %U:%G "$_abs")
                     _size="-"
                     _hash="-"
                 elif [ -d "$_abs" ]; then
                     _type="d"
                     _perms=$(stat -c %A "$_abs")
                     _owner=$(stat -c %U:%G "$_abs")
                     _size="-"
                     _hash="-"
                 elif [ -f "$_abs" ]; then
                     _type="f"
                     _perms=$(stat -c %A "$_abs")
                     _owner=$(stat -c %U:%G "$_abs")
                     _size=$(stat -c %s "$_abs")
                     _hash=$(b3sum "$_abs" | cut -d' ' -f1)
                     case "$_fpath" in /etc/*) _type="c" ;; esac
                 else
                     # Special file or socket, handle as file no hash
                     _type="f"; _perms="-"; _owner="-"; _size="-"; _hash="-"
                 fi
                 
                 if [ "$_perms" != "-" ]; then
                     _perms_oct=$(cvperms "$_perms")
                 else
                     _perms_oct="-"
                 fi
                 
                 echo "$_fpath|$_perms_oct|$_owner|$_type|$_size|$_hash"
             else
                 # File missing from disk, record as blank metadata
                 echo "$_fpath|-|-|-|-|-"
             fi
        fi
    done
}

x_migrate() {
    _detour_db="/var/lib/detour"
    _detour_lock="${_detour_db}/lock"
    _detour_installed="${_detour_db}/installed"

    # 1. Migrate Detour (Legacy) -> SPC
    if [ -d "$_detour_db" ]; then
        msg "migrating from legacy detour at $_detour_db"
        mkdir -p "$SPC_LOCK"
        mkdir -p "$(dirname "$SPC_DB")"

        if [ -d "$_detour_lock" ]; then
            for _lock in "$_detour_lock"/*.lock; do
                [ -f "$_lock" ] || continue
                _base="$(basename "$_lock" .lock)"
                msg "migrating lockfile: $_base"
                _migrate_lockfile_stream "$_base" < "$_lock" > "$SPC_LOCK/$_base.lock"
            done
        fi
        
        if [ -f "$_detour_installed" ]; then
            cat "$_detour_installed" >> "$SPC_DB"
            sort -u "$SPC_DB" -o "$SPC_DB"
        fi

        mv "$_detour_db" "${_detour_db}.bak"
        msg "detour migration complete (backup at ${_detour_db}.bak)"
    fi
}

# === CACHE ===
x_cache_add() {
    [ -f "$1" ] || die "archive not found: $1"
    mkdir -p "$DTR_CACHE"
    cp "$1" "$DTR_CACHE/${1##*/}" || die "failed to cache"
    msg "cached: ${1##*/}"
}

x_cache_list() {
    [ -d "$DTR_CACHE" ] || { msg 'cache empty'; return 0; }
    # No find, just glob
    for _f in "$DTR_CACHE"/*.spc.*; do
        [ -e "$_f" ] || continue
        printf '%s\n' "${_f##*/}"
    done
}

x_cache_clean() {
    [ -d "$DTR_CACHE" ] || { msg 'cache empty'; return 0; }
    rm -rf "$DTR_CACHE"
    msg 'cache cleaned'
}

# === REMOTE ADD ===
is_git_url() {
    case "$1" in
        *git@*:*|git://*|*://*.git|*://*/.git) return 0 ;;
    esac
    return 1
}

resolve_git_version() {
    _ver="$1"
    _src="$2"
    if [ "${_ver#git:}" != "$_ver" ]; then
        printf '%s\n' "$_ver"
        return 0
    fi
    [ "$_ver" = "git" ] || { printf '%s\n' "$_ver"; return 0; }
    [ -n "$_src" ] || { printf '%s\n' "$_ver"; return 0; }
    if is_git_url "$_src" && command -v git >/dev/null 2>&1; then
        _commit="$(git ls-remote "$_src" HEAD 2>/dev/null | awk '{print $1}' | head -n1)"
        if [ -n "$_commit" ]; then
            printf 'git:%s\n' "$_commit"
            return 0
        fi
    fi
    printf '%s\n' "$_ver"
}

resolve_port_meta() {
    _pkg="$1"
    _dir="$(find "$SPC_PORTS" -mindepth 2 -maxdepth 2 -type d -name "$_pkg" -print -quit)"
    [ -n "$_dir" ] || return 1
    _cat="$(basename "$(dirname "$_dir")")"
    _ndmake="$_dir/ndmake.sh"
    [ -f "$_ndmake" ] || return 1
    _ver="$(awk -F= '/^VERSION=/{gsub(/"/,"",$2);print $2;exit}' "$_ndmake")"
    _src="$(awk -F= '/^SOURCE=/{gsub(/"/,"",$2);print $2;exit}' "$_ndmake")"
    _rel="$(awk -F= '/^RELEASE=/{gsub(/"/,"",$2);print $2;exit}' "$_ndmake")"
    [ -n "$_ver" ] || return 1
    [ -n "$_rel" ] || _rel="1"
    _ver="$(resolve_git_version "$_ver" "$_src")"
    printf '%s\n%s\n%s\n' "$_cat" "$_ver" "$_rel"
}

x_add() {
    _req="$1"
    _pkg="${_req%%+*}"
    _meta="$(resolve_port_meta "$_pkg")" || die "port not found for $_pkg (set SPC_PORTS)"
    _cat="$(printf '%s' "$_meta" | sed -n 1p)"
    _ver="$(printf '%s' "$_meta" | sed -n 2p)"
    _rel="$(printf '%s' "$_meta" | sed -n 3p)"

    _ver_enc="$(printf '%s' "$_ver" | sed 's/:/%3A/g')"
    _fname="${_pkg}+${_ver}-${_rel}.spc.tzst"
    _url="${SPC_REPO}/${_cat}/${_pkg}%2B${_ver_enc}-${_rel}.spc.tzst"
    _tmpdir="$(mktemp -d /tmp/spc-add.XXXXXX)" || die "failed to create temp dir"
    _tmp="$_tmpdir/$_fname"

    msg "fetching $_url"
    if ! curl -fL -o "$_tmp" "$_url" 2>/dev/null; then
        rm -rf "$_tmpdir"
        die "failed to fetch package"
    fi

    msg "installing ${_fname}"
    x_install "$_tmp" || { rm -rf "$_tmpdir"; die "install failed"; }
    rm -rf "$_tmpdir"
}

# === MAIN ===
usage() {
    cat <<'EOF'
usage: spc [options] <op> [args]

options:
  --rootfs <dir>                  install/remove/list DB against this rootfs
  --ports <dir>                   ports tree for metadata lookups
  --target <triple>               target triple hint (for tooling integration)
  --libc <name>                   libc hint (musl/mlibc/glibc/...)
  --sysroot <dir>                 sysroot hint (for tooling integration)
  -h, --help                      show help

operations:
  add <package>                  fetch and install from repo
  install <archive>              install package
  create <dir> <pkg.spc.tzst>     create package from directory
  remove <package>               remove package
  migrate                        migrate db management from detour to spc
  list                           list installed
  files <package>                list files
  owner <file>                   find owner
  info <package>                 show info
  verify <package>               verify integrity
  cache-add <archive>            add to cache
  cache-list                     list cache
  cache-clean                    clean cache
EOF
    exit 0
}

[ $# -eq 0 ] && usage

while [ $# -gt 0 ]; do
    case "$1" in
        --rootfs)
            [ $# -lt 2 ] && die 'requires argument for --rootfs'
            SPC_ROOT="$2"
            SPC_DB=
            SPC_LOCK=
            shift 2
            ;;
        --ports)
            [ $# -lt 2 ] && die 'requires argument for --ports'
            SPC_PORTS="$2"
            shift 2
            ;;
        --target)
            [ $# -lt 2 ] && die 'requires argument for --target'
            SPC_TARGET="$2"
            shift 2
            ;;
        --libc)
            [ $# -lt 2 ] && die 'requires argument for --libc'
            SPC_LIBC="$2"
            shift 2
            ;;
        --sysroot)
            [ $# -lt 2 ] && die 'requires argument for --sysroot'
            SPC_SYSROOT="$2"
            shift 2
            ;;
        -h|--help|help)
            usage
            ;;
        --)
            shift
            break
            ;;
        -*)
            die "unknown option: $1"
            ;;
        *)
            break
            ;;
    esac
done

[ $# -eq 0 ] && usage
set_spc_paths

case "$1" in
    add)         [ $# -lt 2 ] && die 'requires package'; x_add "$2" ;;
    install)     [ $# -lt 2 ] && die 'requires archive'; x_install "$2" ;;
    create)      [ $# -lt 3 ] && die 'requires directory and output'; x_create "$2" "$3" ;;
    remove)      [ $# -lt 2 ] && die 'requires package'; x_remove "$2" ;;
    migrate)     x_migrate ;;
    list)        x_list ;;
    files)       [ $# -lt 2 ] && die 'requires package'; x_files "$2" ;;
    owner)       [ $# -lt 2 ] && die 'requires file'; x_owner "$2" || msg 'no owner' ;;
    info)        [ $# -lt 2 ] && die 'requires package'; x_info "$2" ;;
    verify)      [ $# -lt 2 ] && die 'requires package'; x_verify "$2" ;;
    cache-add)   [ $# -lt 2 ] && die 'requires archive'; x_cache_add "$2" ;;
    cache-list)  x_cache_list ;;
    cache-clean) x_cache_clean ;;
    -h|--help|help) usage ;;
    *) die "unknown operation: $1" ;;
esac
