#!/bin/bash

# api-neocities.sh - v1.0 - Sun, 04 Oct 2026 15:22:52 -0300
#
# Copyright (c) 2026 Daniel Dias Rodrigues <danieldiasr@gmail.com>
#
# Assisted by ChatGPT (GPT-5.6 Sol)
#
# This program is free software; you can redistribute it and/or modify it
# under the terms of the Creative Commons Zero 1.0 Universal (CC0 1.0) Public
# Domain Dedication (https://creativecommons.org/publicdomain/zero/1.0/).

set -euo pipefail

API_BASE="https://neocities.org/api"
API_KEY="${NEOCITIES_API_KEY:-}"

PROGRAM="${0##*/}"

usage() {
    cat <<EOF
Usage:
  $PROGRAM <command> [arguments]

Commands:
  download all
      Download all remote files recursively into the current directory.

  download <remote-file> [local-file]
      Download one remote file.
      If local-file is omitted, the remote path is used locally.

  upload all
      Upload all files under the current directory recursively.
      The script itself ($PROGRAM) is ignored.
      Existing remote files are overwritten.

  upload <local-file> [remote-file]
      Upload one local file.
      If remote-file is omitted, the local path is used remotely.

  list [remote-directory]
      List remote files and directories.
      With no directory, list the entire site recursively.

  mkdir <remote-directory>
      Create a remote directory, including missing parent directories.

  rename <old-path> <new-path>
      Rename or move a remote file or directory.

  delete <remote-path> [remote-path ...]
      Delete one or more remote files or directories.
      WARNING: directory deletion is recursive and cannot be undone.

  info [sitename]
      Show site information.
      With sitename, request public information for that site.
      Without sitename, use the authenticated site.

  hash <local-file> [remote-file]
      Compare the SHA-1 hash of one local file with the remote copy.
      If remote-file is omitted, the local path is used remotely.

  hash all
      Compare SHA-1 hashes for all local files recursively.
      The script itself ($PROGRAM) is ignored.

  -h, --help, help
      Show this help.

Authentication:
  Set your Neocities API key in the NEOCITIES_API_KEY environment variable:

      export NEOCITIES_API_KEY='your-api-key'

Examples:
  $PROGRAM download all
  $PROGRAM download index.html
  $PROGRAM download css/style.css backup/style.css

  $PROGRAM upload all
  $PROGRAM upload index.html
  $PROGRAM upload local.html pages/index.html

  $PROGRAM list
  $PROGRAM list images

  $PROGRAM mkdir images/icons
  $PROGRAM rename old.html archive/old.html
  $PROGRAM delete old.html images/unused

  $PROGRAM info
  $PROGRAM info example

  $PROGRAM hash index.html
  $PROGRAM hash all

Requirements:
  curl
  jq    Required for "list", "download all" and "hash all".
  column
  sha1sum or shasum
EOF
}

die() {
    echo "$PROGRAM: $*" >&2
    exit 1
}

require_command() {
    command -v "$1" >/dev/null 2>&1 || die "required command not found: $1"
}

require_auth() {
    [ -n "$API_KEY" ] || die "NEOCITIES_API_KEY is not set"
}

auth_args=()

set_auth_args() {
    require_auth
    auth_args=(-H "Authorization: Bearer $API_KEY")
}

pretty_json() {
    if command -v jq >/dev/null 2>&1; then
        jq .
    else
        cat
    fi
}

normalize_local_path() {
    local path="$1"
    path="${path#./}"
    printf '%s' "$path"
}

sha1_file() {
    local file="$1"

    if command -v sha1sum >/dev/null 2>&1; then
        sha1sum -- "$file" | awk '{print $1}'
    elif command -v shasum >/dev/null 2>&1; then
        shasum -a 1 -- "$file" | awk '{print $1}'
    else
        die "sha1sum or shasum is required"
    fi
}

download_one() {
    local remote="$1"
    local local_path="${2:-$1}"

    [ -n "$remote" ] || die "remote file path is empty"
    [ -n "$local_path" ] || die "local file path is empty"

    mkdir -p -- "$(dirname -- "$local_path")"

    echo "Downloading: $remote -> $local_path"
    curl --fail-with-body -sS \
        "${auth_args[@]}" \
        --get \
        --data-urlencode "path=$remote" \
        -o "$local_path" \
        "$API_BASE/download"
}

download_all() {
    require_command jq

    local response
    response="$(curl --fail-with-body -sS \
        "${auth_args[@]}" \
        "$API_BASE/list")"

    while IFS= read -r remote; do
        download_one "$remote" "$remote"
    done < <(
        printf '%s' "$response" |
            jq -r '.files[] | select(.is_directory == false) | .path'
    )
}

upload_one() {
    local local_path="$1"
    local remote="${2:-$(normalize_local_path "$1")}"

    [ -f "$local_path" ] || die "local file not found: $local_path"
    [ -n "$remote" ] || die "remote file path is empty"

    echo "Uploading: $local_path -> $remote"
    curl --fail-with-body -sS \
        "${auth_args[@]}" \
        -F "$remote=@$local_path" \
        "$API_BASE/upload" |
        pretty_json
}

upload_all() {
    local -a args=()
    local file remote
    local count=0

    while IFS= read -r -d '' file; do
        remote="$(normalize_local_path "$file")"
        echo "Adding: $remote"
        args+=(-F "$remote=@$file")
        count=$((count + 1))
    done < <(find . -type f ! -name "$PROGRAM" -print0)

    [ "$count" -gt 0 ] || die "no files to upload"

    echo "Uploading $count file(s)..."
    curl --fail-with-body -sS \
        "${auth_args[@]}" \
        "${args[@]}" \
        "$API_BASE/upload" |
        pretty_json
}

list_remote() {
    require_command jq

    local response

    if [ "$#" -eq 0 ]; then
        response="$(curl --fail-with-body -sS \
            "${auth_args[@]}" \
            "$API_BASE/list")"
    else
        response="$(curl --fail-with-body -sS \
            "${auth_args[@]}" \
            --get \
            --data-urlencode "path=$1" \
            "$API_BASE/list")"
    fi

    printf '%s\n' "$response" |
        jq -r '
            ["T", "PATH", "SIZE", "CREATED", "UPDATED", "SHA1"],
            (.files[] | [
                (if .is_directory then "d" else "f" end),
                .path,
                (.size // 0 | tostring),
                (.created_at // ""),
                (.updated_at // ""),
                (.sha1_hash // "")
            ])
            | @tsv
        ' |
        column -t -s $'\t'
}

create_directory() {
    local path="$1"

    curl --fail-with-body -sS \
        "${auth_args[@]}" \
        --data-urlencode "path=$path" \
        "$API_BASE/create_directory" |
        pretty_json
}

rename_remote() {
    local old_path="$1"
    local new_path="$2"

    curl --fail-with-body -sS \
        "${auth_args[@]}" \
        --data-urlencode "path=$old_path" \
        --data-urlencode "new_path=$new_path" \
        "$API_BASE/rename" |
        pretty_json
}

delete_remote() {
    local -a args=()
    local path

    [ "$#" -gt 0 ] || die "delete requires at least one remote path"

    for path in "$@"; do
        args+=(--data-urlencode "filenames[]=$path")
    done

    curl --fail-with-body -sS \
        "${auth_args[@]}" \
        "${args[@]}" \
        "$API_BASE/delete" |
        pretty_json
}

site_info() {
    if [ "$#" -eq 0 ]; then
        curl --fail-with-body -sS \
            "${auth_args[@]}" \
            "$API_BASE/info" |
            pretty_json
    else
        curl --fail-with-body -sS \
            --get \
            --data-urlencode "sitename=$1" \
            "$API_BASE/info" |
            pretty_json
    fi
}

hash_one() {
    local local_path="$1"
    local remote="${2:-$(normalize_local_path "$1")}"
    local hash

    [ -f "$local_path" ] || die "local file not found: $local_path"

    hash="$(sha1_file "$local_path")"

    curl --fail-with-body -sS \
        "${auth_args[@]}" \
        --data-urlencode "$remote=$hash" \
        "$API_BASE/upload_hash" |
        pretty_json
}

hash_all() {
    require_command jq

    local -a args=()
    local file remote hash
    local count=0

    while IFS= read -r -d '' file; do
        remote="$(normalize_local_path "$file")"
        hash="$(sha1_file "$file")"
        args+=(--data-urlencode "$remote=$hash")
        count=$((count + 1))
    done < <(find . -type f ! -name "$PROGRAM" -print0)

    [ "$count" -gt 0 ] || die "no files to hash"

    curl --fail-with-body -sS \
        "${auth_args[@]}" \
        "${args[@]}" \
        "$API_BASE/upload_hash" |
        pretty_json
}

main() {
    require_command curl

    case "${1:-}" in
        -h|--help|help)
            usage
            ;;

        download)
            [ "$#" -ge 2 ] || die "usage: $PROGRAM download all | <remote-file> [local-file]"
            set_auth_args

            if [ "$2" = "all" ]; then
                [ "$#" -eq 2 ] || die "usage: $PROGRAM download all"
                download_all
            else
                [ "$#" -le 3 ] || die "usage: $PROGRAM download <remote-file> [local-file]"
                download_one "$2" "${3:-$2}"
            fi
            ;;

        upload)
            [ "$#" -ge 2 ] || die "usage: $PROGRAM upload all | <local-file> [remote-file]"
            set_auth_args

            if [ "$2" = "all" ]; then
                [ "$#" -eq 2 ] || die "usage: $PROGRAM upload all"
                upload_all
            else
                [ "$#" -le 3 ] || die "usage: $PROGRAM upload <local-file> [remote-file]"
                upload_one "$2" "${3:-$(normalize_local_path "$2")}"
            fi
            ;;

        list)
            [ "$#" -le 2 ] || die "usage: $PROGRAM list [remote-directory]"
            set_auth_args
            if [ "$#" -eq 2 ]; then
                list_remote "$2"
            else
                list_remote
            fi
            ;;

        mkdir|create-directory|create_directory)
            [ "$#" -eq 2 ] || die "usage: $PROGRAM mkdir <remote-directory>"
            set_auth_args
            create_directory "$2"
            ;;

        rename|move|mv)
            [ "$#" -eq 3 ] || die "usage: $PROGRAM rename <old-path> <new-path>"
            set_auth_args
            rename_remote "$2" "$3"
            ;;

        delete|rm)
            [ "$#" -ge 2 ] || die "usage: $PROGRAM delete <remote-path> [remote-path ...]"
            set_auth_args
            shift
            delete_remote "$@"
            ;;

        info)
            [ "$#" -le 2 ] || die "usage: $PROGRAM info [sitename]"
            if [ "$#" -eq 2 ]; then
                site_info "$2"
            else
                set_auth_args
                site_info
            fi
            ;;

        hash)
            [ "$#" -ge 2 ] || die "usage: $PROGRAM hash all | <local-file> [remote-file]"
            set_auth_args

            if [ "$2" = "all" ]; then
                [ "$#" -eq 2 ] || die "usage: $PROGRAM hash all"
                hash_all
            else
                [ "$#" -le 3 ] || die "usage: $PROGRAM hash <local-file> [remote-file]"
                hash_one "$2" "${3:-$(normalize_local_path "$2")}"
            fi
            ;;

        "")
            usage
            exit 1
            ;;

        *)
            die "unknown command: $1 (use --help)"
            ;;
    esac
}

main "$@"
