#!/bin/sh
# Taida Lang installer
#
# Usage:
#   curl -fsSL https://taida.dev/install.sh | sh
#   curl -fsSL https://taida.dev/install.sh | sh -s -- @c.15.rc3       # specific version
#   curl -fsSL https://taida.dev/install.sh | sh -s -- --system        # /usr/local/bin (requires sudo)
#   curl -fsSL https://taida.dev/install.sh | sh -s -- --prefix ~/bin  # custom path
#
# Environment variables:
#   TAIDA_HOME          Install prefix (default: $HOME/.taida). Binary lands at
#                       $TAIDA_HOME/bin/taida.
#   TAIDA_INSTALL_DIR   Explicit install directory. Overrides TAIDA_HOME.
#                       (Preserved for backward compatibility.)
#   TAIDA_VERSION       Same as the positional version argument.
#   TAIDA_REPO          GitHub owner/repo (default: taida-lang/taida).
#
# Default install destination is $HOME/.taida/bin, a user-owned directory
# that requires no sudo. This matches the `rustup` / `bun` / `deno`
# convention and keeps `taida upgrade` sudo-free (self-replace writes
# the same path as the installed binary). Use --system only if a
# system-wide install is explicitly required.

set -eu

REPO="${TAIDA_REPO:-taida-lang/taida}"
TAIDA_HOME="${TAIDA_HOME:-$HOME/.taida}"
BINARY_NAME="taida"
VERSION="${TAIDA_VERSION:-}"
PLATFORM=""
TARGET=""
ARCHIVE_EXT=""
TMPDIR_ROOT=""

# Install destination. Decided in parse_args after flags are consumed.
INSTALL_DIR=""
INSTALL_MODE=""   # user | system | explicit

# Shell-profile modification. Default: auto-append for user-mode installs
# (matches rustup / bun / deno behaviour). Disabled by --no-modify-path
# or TAIDA_NO_MODIFY_PATH=1.
MODIFY_PATH="${TAIDA_NO_MODIFY_PATH:+0}"

say() {
    printf '[taida] %s\n' "$1"
}

err() {
    printf '[taida] ERROR: %s\n' "$1" >&2
    exit 1
}

have_cmd() {
    command -v "$1" >/dev/null 2>&1
}

need_cmd() {
    if ! have_cmd "$1"; then
        err "need '$1' (command not found)"
    fi
}

cleanup() {
    if [ -n "${TMPDIR_ROOT}" ] && [ -d "${TMPDIR_ROOT}" ]; then
        rm -rf "${TMPDIR_ROOT}"
    fi
}

download() {
    url="$1"
    output="$2"

    if have_cmd curl; then
        curl -fsSL "$url" -o "$output"
    elif have_cmd wget; then
        wget -qO "$output" "$url"
    else
        err "need 'curl' or 'wget' (command not found)"
    fi
}

compute_sha256() {
    file="$1"

    if have_cmd sha256sum; then
        sha256sum "$file" | awk '{print $1}'
    elif have_cmd shasum; then
        shasum -a 256 "$file" | awk '{print $1}'
    elif have_cmd openssl; then
        openssl dgst -sha256 "$file" | sed 's/^.*= //'
    else
        err "need 'sha256sum', 'shasum', or 'openssl' (command not found)"
    fi
}

extract_archive() {
    archive_path="$1"
    output_dir="$2"

    case "$ARCHIVE_EXT" in
        tar.gz)
            need_cmd tar
            tar xzf "$archive_path" -C "$output_dir"
            ;;
        zip)
            need_cmd unzip
            unzip -q "$archive_path" -d "$output_dir"
            ;;
        *)
            err "unsupported archive extension: $ARCHIVE_EXT"
            ;;
    esac
}

# Ensure $INSTALL_DIR exists. Uses sudo only if both the parent
# directory is not user-writable AND we are in system mode.
ensure_install_dir() {
    if [ -d "$INSTALL_DIR" ]; then
        return 0
    fi

    if mkdir -p "$INSTALL_DIR" 2>/dev/null; then
        return 0
    fi

    # mkdir failed — fall back to sudo if we're actually in system mode
    if [ "$INSTALL_MODE" = "system" ] || [ "$INSTALL_MODE" = "explicit" ]; then
        need_cmd sudo
        say "creating ${INSTALL_DIR} (requires sudo)..."
        sudo mkdir -p "$INSTALL_DIR" || err "failed to create ${INSTALL_DIR}"
        return 0
    fi

    err "failed to create ${INSTALL_DIR}"
}

install_binary() {
    binary_path="$1"

    ensure_install_dir

    chmod +x "$binary_path"

    if [ -w "$INSTALL_DIR" ]; then
        mv "$binary_path" "${INSTALL_DIR}/${BINARY_NAME}"
    else
        need_cmd sudo
        say "installing to ${INSTALL_DIR} (requires sudo)..."
        sudo mv "$binary_path" "${INSTALL_DIR}/${BINARY_NAME}" || err "installation failed"
    fi

    say "installed ${BINARY_NAME} ${VERSION} to ${INSTALL_DIR}/${BINARY_NAME}"
}

detect_platform() {
    os="$(uname -s)"
    arch="$(uname -m)"

    case "$os" in
        Linux)  os="linux" ;;
        Darwin) os="macos" ;;
        *)
            PLATFORM="${os}-${arch}"
            return
            ;;
    esac

    case "$arch" in
        x86_64|amd64)  arch="x86_64" ;;
        aarch64|arm64) arch="aarch64" ;;
        *)
            PLATFORM="${os}-${arch}"
            return
            ;;
    esac

    PLATFORM="${os}-${arch}"

    case "$PLATFORM" in
        linux-x86_64)
            TARGET="x86_64-unknown-linux-gnu"
            ARCHIVE_EXT="tar.gz"
            ;;
        macos-x86_64)
            TARGET="x86_64-apple-darwin"
            ARCHIVE_EXT="tar.gz"
            ;;
        macos-aarch64)
            TARGET="aarch64-apple-darwin"
            ARCHIVE_EXT="tar.gz"
            ;;
        *)
            TARGET=""
            ARCHIVE_EXT=""
            ;;
    esac
}

fetch_latest_version() {
    api_url="https://api.github.com/repos/${REPO}/releases/latest"
    response_path="${TMPDIR_ROOT}/release.json"

    download "$api_url" "$response_path" || err "failed to fetch latest release from GitHub"

    VERSION="$(sed -n 's/.*"tag_name"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$response_path" | head -n 1)"

    if [ -z "$VERSION" ]; then
        err "could not determine latest version"
    fi

    say "latest version: $VERSION"
}

download_release_asset() {
    asset_name="$1"
    destination="$2"
    download_url="https://github.com/${REPO}/releases/download/${VERSION}/${asset_name}"

    if download "$download_url" "$destination"; then
        return 0
    fi

    return 1
}

verify_checksum() {
    archive_name="$1"
    archive_path="$2"
    sums_path="${TMPDIR_ROOT}/SHA256SUMS"
    expected_checksum=""
    actual_checksum=""

    download_release_asset "SHA256SUMS" "$sums_path" || err "failed to download SHA256SUMS for ${VERSION}"

    expected_checksum="$(awk -v file="$archive_name" '$2 == file { print $1; exit }' "$sums_path")"

    if [ -z "$expected_checksum" ]; then
        err "checksum not found for ${archive_name}"
    fi

    actual_checksum="$(compute_sha256 "$archive_path")"

    if [ "$expected_checksum" != "$actual_checksum" ]; then
        err "checksum verification failed for ${archive_name}"
    fi

    say "verified checksum for ${archive_name}"
}

find_extracted_binary() {
    search_root="$1"
    found_path="$(find "$search_root" -type f -name "$BINARY_NAME" | head -n 1)"

    if [ -z "$found_path" ]; then
        err "binary not found after extraction"
    fi

    printf '%s\n' "$found_path"
}

download_prebuilt() {
    if [ -z "$TARGET" ] || [ -z "$ARCHIVE_EXT" ]; then
        return 1
    fi

    archive_name="taida-${VERSION}-${TARGET}.${ARCHIVE_EXT}"
    archive_path="${TMPDIR_ROOT}/${archive_name}"
    extract_dir="${TMPDIR_ROOT}/prebuilt"

    say "trying prebuilt artifact for ${TARGET}"
    if ! download_release_asset "$archive_name" "$archive_path"; then
        say "prebuilt artifact not available for ${TARGET}; falling back to source build"
        return 1
    fi

    verify_checksum "$archive_name" "$archive_path"
    mkdir -p "$extract_dir"
    extract_archive "$archive_path" "$extract_dir"
    install_binary "$(find_extracted_binary "$extract_dir")"
    return 0
}

build_from_source() {
    source_archive="${TMPDIR_ROOT}/source.tar.gz"
    source_root="${TMPDIR_ROOT}/source"
    source_url="https://github.com/${REPO}/archive/refs/tags/${VERSION}.tar.gz"

    need_cmd cargo
    need_cmd tar

    say "building from source for ${PLATFORM}"
    download "$source_url" "$source_archive" || err "failed to download source archive for ${VERSION}"
    mkdir -p "$source_root"
    tar xzf "$source_archive" -C "$source_root"

    source_dir="$(find "$source_root" -mindepth 1 -maxdepth 1 -type d | head -n 1)"
    if [ -z "$source_dir" ]; then
        err "failed to locate extracted source directory"
    fi

    (
        cd "$source_dir"
        cargo build --locked --release
    ) || err "source build failed"

    install_binary "${source_dir}/target/release/${BINARY_NAME}"
}

# Decide which profile file to modify and what line to append.
# Sets SHELL_PROFILE and SHELL_PROFILE_LINE. Empty values mean
# "shell unknown — fall back to instructions only".
SHELL_PROFILE=""
SHELL_PROFILE_LINE=""
resolve_shell_profile() {
    case "${SHELL:-}" in
        */fish)
            SHELL_PROFILE="$HOME/.config/fish/config.fish"
            SHELL_PROFILE_LINE="fish_add_path ${INSTALL_DIR}"
            ;;
        */zsh)
            SHELL_PROFILE="$HOME/.zshrc"
            SHELL_PROFILE_LINE="export PATH=\"${INSTALL_DIR}:\$PATH\""
            ;;
        */bash)
            # macOS login shells read .bash_profile; Linux interactive
            # shells read .bashrc. Pick the one that matches convention.
            if [ "$(uname -s)" = "Darwin" ]; then
                SHELL_PROFILE="$HOME/.bash_profile"
            else
                SHELL_PROFILE="$HOME/.bashrc"
            fi
            SHELL_PROFILE_LINE="export PATH=\"${INSTALL_DIR}:\$PATH\""
            ;;
        *)
            SHELL_PROFILE=""
            SHELL_PROFILE_LINE="export PATH=\"${INSTALL_DIR}:\$PATH\""
            ;;
    esac
}

# Append PATH export to the user's shell profile if:
#   - INSTALL_DIR is not already on PATH
#   - the profile doesn't already mention INSTALL_DIR
#   - auto-modification is not disabled (--no-modify-path / TAIDA_NO_MODIFY_PATH)
#
# Returns:
#   0 if we appended a line (caller should show source instruction)
#   1 if nothing was written (already on PATH, opted out, or unknown shell)
append_path_if_needed() {
    case ":${PATH}:" in
        *":${INSTALL_DIR}:"*) return 1 ;;
    esac

    if [ "${MODIFY_PATH:-1}" = "0" ]; then
        return 1
    fi

    resolve_shell_profile

    if [ -z "$SHELL_PROFILE" ]; then
        # Unknown shell — don't touch any rc file, caller will print hints.
        return 1
    fi

    if [ -f "$SHELL_PROFILE" ] && grep -qsF "${INSTALL_DIR}" "$SHELL_PROFILE"; then
        say "PATH already configured in $SHELL_PROFILE (${INSTALL_DIR} referenced)"
        return 1
    fi

    mkdir -p "$(dirname "$SHELL_PROFILE")" || return 1
    {
        printf '\n'
        printf '# Added by Taida Lang installer (taida.dev/install.sh)\n'
        printf '%s\n' "$SHELL_PROFILE_LINE"
    } >> "$SHELL_PROFILE" || return 1

    say "appended PATH entry to $SHELL_PROFILE"
    return 0
}

# Fallback: print manual PATH setup hints. Used when auto-append was
# declined (unknown shell or --no-modify-path).
print_path_instructions() {
    resolve_shell_profile
    say ""
    if [ -n "$SHELL_PROFILE" ]; then
        say "Add this line to $SHELL_PROFILE and restart your shell:"
    else
        say "Add this line to your shell profile and restart your shell:"
    fi
    say "  $SHELL_PROFILE_LINE"
}

# Tell the user how to activate the PATH change in the current shell.
print_source_instruction() {
    if [ -z "$SHELL_PROFILE" ]; then
        return 0
    fi
    say ""
    case "${SHELL:-}" in
        */fish)
            say "Activate in the current shell:"
            say "  source $SHELL_PROFILE"
            ;;
        *)
            say "Activate in the current shell (pick one):"
            say "  source $SHELL_PROFILE"
            say "  . $SHELL_PROFILE"
            ;;
    esac
    say "…or open a new terminal."
}

verify() {
    if command -v "$BINARY_NAME" >/dev/null 2>&1; then
        installed_path="$(command -v "$BINARY_NAME")"
        say "verification: $("$installed_path" --version 2>/dev/null || echo 'ok')"
        # If PATH lookup landed on a different path than the one we just
        # installed, warn — the user probably has a stale binary earlier
        # on PATH (the classic ~/.cargo/bin/taida vs /usr/local/bin/taida
        # situation).
        if [ "$installed_path" != "${INSTALL_DIR}/${BINARY_NAME}" ]; then
            say ""
            say "note: 'taida' on PATH resolves to ${installed_path},"
            say "      not the freshly installed ${INSTALL_DIR}/${BINARY_NAME}."
            say "      Earlier entries in your PATH take priority; remove or reorder them to pick up this install."
        fi
        return
    fi

    if [ -x "${INSTALL_DIR}/${BINARY_NAME}" ]; then
        say ""
        say "${BINARY_NAME} is installed at ${INSTALL_DIR}/${BINARY_NAME} but is not on PATH yet."
        if append_path_if_needed; then
            print_source_instruction
        else
            print_path_instructions
        fi
    else
        err "installation verification failed: ${INSTALL_DIR}/${BINARY_NAME} not found"
    fi
}

print_usage() {
    cat <<'USAGE'
Taida Lang installer

Usage:
  sh install.sh [OPTIONS] [VERSION]

Options:
  --system              Install to /usr/local/bin (requires sudo).
  --prefix PATH         Install to PATH (no sudo unless PATH is not user-writable).
  --no-modify-path      Do not append a PATH entry to your shell profile.
  -h, --help            Print this help and exit.

Environment:
  TAIDA_HOME            Install prefix root (default: $HOME/.taida).
                        Binary lands at $TAIDA_HOME/bin/taida.
  TAIDA_INSTALL_DIR     Explicit install directory. Overrides TAIDA_HOME.
  TAIDA_VERSION         Version tag, same as the positional argument.
  TAIDA_REPO            GitHub owner/repo (default: taida-lang/taida).
  TAIDA_NO_MODIFY_PATH  Set to any value to skip shell-profile PATH modification
                        (equivalent to --no-modify-path).

Examples:
  sh install.sh                        # install latest to $HOME/.taida/bin (default)
  sh install.sh @c.15.rc3              # install specific version to $HOME/.taida/bin
  sh install.sh --system               # install latest to /usr/local/bin (requires sudo)
  sh install.sh --prefix ~/bin         # install latest to ~/bin
  TAIDA_HOME=/opt/taida sh install.sh  # install latest to /opt/taida/bin
USAGE
}

parse_args() {
    while [ "$#" -gt 0 ]; do
        case "$1" in
            --system)
                INSTALL_DIR="/usr/local/bin"
                INSTALL_MODE="system"
                shift
                ;;
            --prefix)
                if [ "$#" -lt 2 ]; then
                    err "--prefix requires a path argument"
                fi
                INSTALL_DIR="$2"
                INSTALL_MODE="explicit"
                shift 2
                ;;
            --no-modify-path)
                MODIFY_PATH="0"
                shift
                ;;
            -h|--help)
                print_usage
                exit 0
                ;;
            -*)
                err "unknown flag: $1 (try --help)"
                ;;
            *)
                if [ -n "$VERSION" ]; then
                    err "multiple version arguments"
                fi
                VERSION="$1"
                shift
                ;;
        esac
    done

    # Resolve default install dir when not set via flag.
    # Priority: --system / --prefix > TAIDA_INSTALL_DIR > TAIDA_HOME/bin.
    if [ -z "$INSTALL_DIR" ]; then
        if [ -n "${TAIDA_INSTALL_DIR:-}" ]; then
            INSTALL_DIR="$TAIDA_INSTALL_DIR"
            INSTALL_MODE="explicit"
        else
            INSTALL_DIR="${TAIDA_HOME}/bin"
            INSTALL_MODE="user"
        fi
    fi
}

main() {
    parse_args "$@"

    TMPDIR_ROOT="$(mktemp -d)" || err "failed to create temp directory"
    trap cleanup EXIT INT TERM

    say "Taida Lang installer"
    say ""

    detect_platform
    say "detected platform: ${PLATFORM}"
    say "install directory: ${INSTALL_DIR}"

    if [ -z "$VERSION" ]; then
        fetch_latest_version
    else
        say "selected version: $VERSION"
    fi

    if ! download_prebuilt; then
        build_from_source
    fi

    verify
    say ""
    say "done. Run 'taida --help' to get started."
}

main "$@"
