#!/bin/bash

# Dotfiles sync script
set -euo pipefail

# Configuration
hostname_id=$(uname -n)
checkout_dir=$HOME/.dofiles
script_dir=$checkout_dir/dotfiles
dots_dir=$checkout_dir/dotfiles/dots
dots_host_dir=$checkout_dir/dotfiles/dots.$hostname_id
repo="https://git.capella.pro/capella/dotfiles.git"
ProgName=$(basename "$0")

# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color

# Logging functions
log_info() { echo -e "${GREEN}[INFO]${NC} $*"; }
log_warn() { echo -e "${YELLOW}[WARN]${NC} $*" >&2; }
log_error() { echo -e "${RED}[ERROR]${NC} $*" >&2; }

# Get group names for the current host (sorted, one per line)
get_groups() {
    local groups_file="$script_dir/groups.$hostname_id"
    if [[ -f "$groups_file" ]]; then
        grep -v '^\s*#' "$groups_file" | grep -v '^\s*$' | sort
    fi
}

# Get combined sync list (common + groups + host-specific, deduplicated)
get_sync_list() {
    {
        cat "$script_dir/to_sync" 2>/dev/null || true
        while IFS= read -r group; do
            cat "$script_dir/to_sync.@$group" 2>/dev/null || true
        done < <(get_groups)
        cat "$script_dir/to_sync.$hostname_id" 2>/dev/null || true
    } | grep -v '^\s*$' | sort -u || true
}

# Resolve source path: common -> groups (alpha) -> host-specific (last wins)
resolve_source() {
    local p="$1"
    local result="$dots_dir/$p"

    while IFS= read -r group; do
        if [[ -e "$script_dir/dots.@$group/$p" ]]; then
            result="$script_dir/dots.@$group/$p"
        fi
    done < <(get_groups)

    if [[ -e "$dots_host_dir/$p" ]]; then
        result="$dots_host_dir/$p"
    fi

    echo "$result"
}

# Check if a file contains @host or @group markers
file_has_markers() {
    grep -qE '@(host|group) ' "$1" 2>/dev/null
}

# Check if any file in a directory tree contains markers
dir_has_markers() {
    grep -rqlE '@(host|group) ' "$1" 2>/dev/null
}

# Detect the comment style from the first marker line in a file
# Returns "block" for /* */ style, or the line-comment prefix (# // ; --)
detect_comment_style() {
    local src="$1"
    local marker_line
    marker_line=$(grep -m1 -E '@(host|group) ' "$src")
    # Extract everything before @host or @group
    local prefix="${marker_line%%@*}"
    # Trim trailing whitespace
    prefix="${prefix%"${prefix##*[![:space:]]}"}"
    # Check if it's a block comment opener
    if [[ "$prefix" == "/*" ]]; then
        echo "block"
    else
        echo "$prefix"
    fi
}

# Process a file with @host/@group markers, outputting only matching sections
process_markers() {
    local src="$1"
    local in_block=0
    local include_block=0

    # Pre-load groups into an associative array for fast lookup
    declare -A host_groups
    while IFS= read -r g; do
        host_groups["$g"]=1
    done < <(get_groups)

    while IFS= read -r line || [[ -n "$line" ]]; do
        if [[ $in_block -eq 0 ]]; then
            if [[ "$line" =~ @host[[:space:]]+([^[:space:]]+) ]]; then
                in_block=1
                if [[ "${BASH_REMATCH[1]}" == "$hostname_id" ]]; then
                    include_block=1
                else
                    include_block=0
                fi
            elif [[ "$line" =~ @group[[:space:]]+([^[:space:]]+) ]]; then
                in_block=1
                if [[ -v "host_groups[${BASH_REMATCH[1]}]" ]]; then
                    include_block=1
                else
                    include_block=0
                fi
            elif [[ "$line" =~ @end ]]; then
                # Stray @end outside a block, skip it
                continue
            else
                printf '%s\n' "$line"
            fi
        else
            if [[ "$line" =~ @end ]]; then
                in_block=0
                include_block=0
            elif [[ $include_block -eq 1 ]]; then
                printf '%s\n' "$line"
            fi
        fi
    done < "$src"

    if [[ $in_block -eq 1 ]]; then
        log_error "Unclosed @host/@group block in $src"
        return 1
    fi
}

# Ensure we cleanup on exit
trap 'cd "$HOME"' EXIT

# Create checkout directory if needed
mkdir -p "$checkout_dir"


sub_help(){
    local groups
    groups=$(get_groups | tr '\n' ', ' | sed 's/,$//')
    cat << EOF
Usage: $ProgName <subcommand> [options]

Subcommands:
    sync [--machine|--group <name>] [dot_file]
                                 Sync dotfiles (optionally add a new dotfile)
                                 --machine: add to host-specific config ($hostname_id)
                                 --group <name>: add to group-specific config
    list                         List currently synced dotfiles
    status                       Show git status of dotfiles repo

Host: $hostname_id
Groups: ${groups:-none}
  Resolution order: common -> groups (alpha) -> host-specific

  Common dotfiles:   dots/ + to_sync
  Group dotfiles:    dots.@<group>/ + to_sync.@<group>
  Host overrides:    dots.$hostname_id/ + to_sync.$hostname_id

In-file markers:
  # @host <name> ... # @end   (host-specific block)
  # @group <name> ... # @end  (group-specific block)
  Files with markers are processed and written (not symlinked).

For help with each subcommand run:
    $ProgName <subcommand> -h|--help

EOF
}

sub_sync(){
    # Clone repo if it doesn't exist
    if [[ ! -d "$checkout_dir/dotfiles/.git" ]]; then
        log_info "Cloning dotfiles repository..."
        cd "$checkout_dir"
        if ! git clone "$repo"; then
            log_error "Failed to clone repository"
            exit 1
        fi
        cd "$script_dir"
        git submodule init
        git submodule update
    fi

    # Navigate to script directory
    cd "$script_dir"

    # Get current branch
    current_branch=$(git branch --show-current)
    log_info "Pulling latest changes from $current_branch..."
    git pull origin "$current_branch"
    git submodule update --init --recursive

    # Parse flags for adding new dotfiles
    local machine_specific=0
    local group_target=""
    if [[ "${1:-}" == "--machine" ]]; then
        machine_specific=1
        shift
    elif [[ "${1:-}" == "--group" ]]; then
        group_target="${2:-}"
        if [[ -z "$group_target" ]]; then
            log_error "--group requires a group name"
            exit 1
        fi
        shift 2
    fi

    # Add new dotfile if provided
    if [ -n "${1:-}" ]; then
        local file_to_add="$1"

        # Validate that file exists
        if [[ ! -e "$HOME/$file_to_add" ]]; then
            log_error "File $HOME/$file_to_add does not exist"
            exit 1
        fi

        log_info "Adding new dotfile: $file_to_add"

        real_path=$(realpath "$HOME/$file_to_add")
        to_copy=${real_path#"$HOME/"}

        if [[ $machine_specific -eq 1 ]]; then
            target_dots_dir="$dots_host_dir"
            sync_file="$script_dir/to_sync.$hostname_id"
            log_info "Adding as host-specific ($hostname_id)"
        elif [[ -n "$group_target" ]]; then
            target_dots_dir="$script_dir/dots.@$group_target"
            sync_file="$script_dir/to_sync.@$group_target"
            log_info "Adding as group-specific (@$group_target)"
        else
            target_dots_dir="$dots_dir"
            sync_file="$script_dir/to_sync"
        fi

        # Create parent directory structure
        mkdir -p "$(dirname "$target_dots_dir/$to_copy")"

        # Copy to dots directory
        if cp -R "$real_path" "$target_dots_dir/$to_copy"; then
            log_info "Copied $file_to_add to $target_dots_dir/$to_copy"
        else
            log_error "Failed to copy $file_to_add"
            exit 1
        fi

        # Add to sync file if not already there
        if ! grep -qxF "$to_copy" "$sync_file" 2>/dev/null; then
            echo "$to_copy" >> "$sync_file"
            log_info "Added $to_copy to $(basename "$sync_file")"
        else
            log_info "$to_copy already in $(basename "$sync_file")"
        fi

        # Remove original and create symlink
        rm -rf "$real_path"
        ln -s "$target_dots_dir/$to_copy" "$real_path"
        log_info "Created symlink: $real_path -> $target_dots_dir/$to_copy"
    fi

    # Stage tracked files from all source directories
    log_info "Staging changes..."
    get_sync_list | while IFS= read -r p; do
        if [[ -e "$dots_dir/$p" ]]; then
            git add "$dots_dir/$p"
        fi
        while IFS= read -r group; do
            if [[ -e "$script_dir/dots.@$group/$p" ]]; then
                git add "$script_dir/dots.@$group/$p"
            fi
        done < <(get_groups)
        if [[ -e "$dots_host_dir/$p" ]]; then
            git add "$dots_host_dir/$p"
        fi
    done

    git add start to_sync
    # Stage host-specific, group-specific, and groups files
    for f in to_sync.* groups.*; do
        [[ -f "$f" ]] && git add "$f"
    done
    for d in dots.*/; do
        [[ -d "$d" ]] && git add "$d"
    done

    # Commit and push if there are changes
    if git diff --cached --quiet; then
        log_info "No changes to commit"
    else
        log_info "Committing changes..."
        if git commit -m "Sync: $(date '+%Y-%m-%d %H:%M:%S')"; then
            log_info "Pushing to remote..."
            git remote add origin "$repo" 2>/dev/null || true
            if git push -u origin "$current_branch"; then
                log_info "Successfully pushed changes"
            else
                log_error "Failed to push changes"
                exit 1
            fi
        else
            log_error "Failed to commit changes"
            exit 1
        fi
    fi

    # Deploy a single file: process markers or symlink
    deploy_file() {
        local src="$1"
        local dest="$2"

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

        if file_has_markers "$src"; then
            # File has markers — process and write (not symlink)
            if [[ -L "$dest" ]]; then
                rm "$dest"
            fi

            local comment_style
            comment_style=$(detect_comment_style "$src")

            {
                if [[ "$comment_style" == "block" ]]; then
                    echo "/* DO NOT EDIT - Generated by dotsync from: $src"
                    echo "   Regenerate with: dotsync */"
                else
                    echo "$comment_style DO NOT EDIT - Generated by dotsync from:"
                    echo "$comment_style $src"
                    echo "$comment_style Regenerate with: dotsync"
                fi
                echo ""
                process_markers "$src"
            } > "$dest.dotsync_tmp"

            # Only update if content changed
            if [[ -f "$dest" ]] && cmp -s "$dest" "$dest.dotsync_tmp"; then
                rm "$dest.dotsync_tmp"
            else
                [[ -e "$dest" ]] && rm "$dest"
                mv "$dest.dotsync_tmp" "$dest"
                chmod --reference="$src" "$dest"
                log_info "Processed: $dest (from $src)"
            fi
        else
            # No markers — symlink
            if [[ -L "$dest" ]] && [[ "$(readlink "$dest")" == "$src" ]]; then
                return  # Already correctly linked
            fi

            if [[ -e "$dest" ]] || [[ -L "$dest" ]]; then
                log_warn "Removing existing $dest"
                rm -rf "$dest"
            fi

            if ln -s "$src" "$dest"; then
                log_info "Linked: $dest -> $src"
            else
                log_error "Failed to create symlink for $(basename "$dest")"
                return 1
            fi
        fi
    }

    # Deploy dotfiles (symlink or process markers)
    log_info "Deploying dotfiles for $hostname_id..."
    get_sync_list | while IFS= read -r p; do
        local source
        source=$(resolve_source "$p")
        local destination="$HOME/$p"

        if [[ ! -e "$source" ]]; then
            log_warn "Source $source does not exist, skipping"
            continue
        fi

        if [[ -d "$source" ]]; then
            if dir_has_markers "$source"; then
                # Directory contains markers — expand into per-file operations
                # Remove directory-level symlink if present
                if [[ -L "$destination" ]]; then
                    log_warn "Replacing directory symlink $destination with expanded files"
                    rm "$destination"
                fi
                mkdir -p "$destination"

                # Walk all files in the source directory
                while IFS= read -r src_file; do
                    local rel="${src_file#"$source/"}"
                    deploy_file "$src_file" "$destination/$rel"
                done < <(find "$source" -type f | sort)
            else
                # No markers in directory — symlink the whole directory
                if [[ -L "$destination" ]] && [[ "$(readlink "$destination")" == "$source" ]]; then
                    continue  # Already correctly linked
                fi

                if [[ -e "$destination" ]] || [[ -L "$destination" ]]; then
                    log_warn "Removing existing $destination"
                    rm -rf "$destination"
                fi

                mkdir -p "$(dirname "$destination")"
                if ln -s "$source" "$destination"; then
                    log_info "Linked: $destination -> $source"
                else
                    log_error "Failed to create symlink for $p"
                    exit 1
                fi
            fi
        else
            # Single file entry
            deploy_file "$source" "$destination"
        fi
    done

    log_info "Sync complete!"
}

sub_list(){
    echo "Host: $hostname_id"

    local groups
    groups=$(get_groups | tr '\n' ', ' | sed 's/,$//')
    if [[ -n "$groups" ]]; then
        echo "Groups: $groups"
    fi
    echo ""

    if [[ -f "$script_dir/to_sync" ]]; then
        echo "Common dotfiles (to_sync):"
        while IFS= read -r p; do
            [[ -z "$p" ]] && continue
            local source
            source=$(resolve_source "$p")
            local tag=""
            if [[ "$source" == "$dots_host_dir/"* ]]; then
                tag=" [override: $hostname_id]"
            elif [[ "$source" == *"/dots.@"* ]]; then
                local gname
                gname=$(echo "$source" | sed 's|.*dots\.@\([^/]*\)/.*|\1|')
                tag=" [override: @$gname]"
            fi
            local marker_tag=""
            if [[ -f "$source" ]] && file_has_markers "$source"; then
                marker_tag=" [processed]"
            fi
            if [[ -L "$HOME/$p" ]] || [[ -f "$HOME/$p" ]] || [[ -d "$HOME/$p" ]]; then
                echo "  + $p$tag$marker_tag"
            else
                echo "  - $p (not deployed)$tag$marker_tag"
            fi
        done < "$script_dir/to_sync"
    fi

    # Group-specific dotfiles
    while IFS= read -r group; do
        local gsync="$script_dir/to_sync.@$group"
        if [[ -f "$gsync" ]] && [[ -s "$gsync" ]]; then
            echo ""
            echo "Group dotfiles (to_sync.@$group):"
            while IFS= read -r p; do
                [[ -z "$p" ]] && continue
                if [[ -L "$HOME/$p" ]] || [[ -f "$HOME/$p" ]] || [[ -d "$HOME/$p" ]]; then
                    echo "  + $p"
                else
                    echo "  - $p (not deployed)"
                fi
            done < "$gsync"
        fi
    done < <(get_groups)

    # Host-specific dotfiles
    if [[ -f "$script_dir/to_sync.$hostname_id" ]] && [[ -s "$script_dir/to_sync.$hostname_id" ]]; then
        echo ""
        echo "Host-specific dotfiles (to_sync.$hostname_id):"
        while IFS= read -r p; do
            [[ -z "$p" ]] && continue
            if [[ -L "$HOME/$p" ]] || [[ -f "$HOME/$p" ]] || [[ -d "$HOME/$p" ]]; then
                echo "  + $p"
            else
                echo "  - $p (not deployed)"
            fi
        done < "$script_dir/to_sync.$hostname_id"
    fi
}

sub_status(){
    if [[ ! -d "$script_dir/.git" ]]; then
        log_error "Dotfiles repository not found"
        exit 1
    fi

    cd "$script_dir"
    log_info "Host: $hostname_id"
    log_info "Git status:"
    git status
}


subcommand=${1:-}
case $subcommand in
    "" | "-h" | "--help")
        sub_help
        ;;
    sync|list|status)
        shift
        "sub_${subcommand}" "$@"
        ;;
    *)
        log_error "'$subcommand' is not a known subcommand."
        echo "       Run '$ProgName --help' for a list of known subcommands." >&2
        exit 1
        ;;
esac
