{ config, lib, pkgs, ... }:

let
  cfg = config.services.git-autosync;

  git-autosync-script = pkgs.writeShellScript "git-autosync" ''
    set -euo pipefail

    REPO_DIR="$1"
    DEBOUNCE_SECONDS="''${2:-5}"
    PULL_INTERVAL_SECONDS="''${3:-120}"
    BRANCH="''${4:-main}"
    MAX_PULL_RETRIES="''${5:-6}"
    RETRY_DELAY_SECONDS="''${6:-4}"

    cd "$REPO_DIR"

    # Ensure we're in a git repo
    if ! ${pkgs.git}/bin/git rev-parse --is-inside-work-tree &>/dev/null; then
      echo "ERROR: $REPO_DIR is not a git repository"
      exit 1
    fi

    send_notification() {
      local urgency="$1"
      local title="$2"
      local message="$3"
      ${pkgs.libnotify}/bin/notify-send -u "$urgency" "$title" "$message" 2>/dev/null || true
    }

    commit_and_push() {
      cd "$REPO_DIR"
      ${pkgs.git}/bin/git add -A
      if ! ${pkgs.git}/bin/git diff --cached --quiet; then
        ${pkgs.git}/bin/git commit -m "autosync: $(date -Iseconds) on $(hostname)"
        echo "Committed changes"
      fi

      if ${pkgs.git}/bin/git remote get-url origin &>/dev/null; then
        ${pkgs.git}/bin/git push origin "$BRANCH" 2>/dev/null && echo "Pushed" || echo "Push failed (offline?)"
      fi
    }

    # pull_remote returns 0 on success, 1 on failure
    pull_remote() {
      cd "$REPO_DIR"
      if ${pkgs.git}/bin/git remote get-url origin &>/dev/null; then
        ${pkgs.git}/bin/git fetch origin "$BRANCH" 2>/dev/null || { echo "Fetch failed (offline?)"; return 1; }

        LOCAL=$(${pkgs.git}/bin/git rev-parse HEAD)
        REMOTE=$(${pkgs.git}/bin/git rev-parse "origin/$BRANCH" 2>/dev/null || echo "")

        if [ -n "$REMOTE" ] && [ "$LOCAL" != "$REMOTE" ]; then
          # Stash any uncommitted changes, rebase, then pop
          STASHED=false
          if ! ${pkgs.git}/bin/git diff --quiet || ! ${pkgs.git}/bin/git diff --cached --quiet; then
            ${pkgs.git}/bin/git stash push -m "autosync-temp"
            STASHED=true
          fi

          ${pkgs.git}/bin/git rebase "origin/$BRANCH" || {
            echo "Rebase conflict — aborting rebase, committing local state"
            ${pkgs.git}/bin/git rebase --abort
          }

          if [ "$STASHED" = true ]; then
            ${pkgs.git}/bin/git stash pop || {
              echo "Stash pop conflict — keeping stash, check manually"
            }
          fi
          echo "Pulled remote changes"
        fi
        return 0
      fi
      return 0  # No remote configured, not a failure
    }

    # Initial pull with exponential backoff - must succeed before we start watching
    PULL_ATTEMPTS=0
    CURRENT_DELAY="$RETRY_DELAY_SECONDS"
    while true; do
      if pull_remote; then
        echo "Initial pull succeeded"
        break
      fi

      PULL_ATTEMPTS=$((PULL_ATTEMPTS + 1))
      echo "Initial pull failed (attempt $PULL_ATTEMPTS/$MAX_PULL_RETRIES)"

      if [ "$PULL_ATTEMPTS" -ge "$MAX_PULL_RETRIES" ]; then
        send_notification "critical" "git-autosync failed: $REPO_DIR" "Unable to pull after $MAX_PULL_RETRIES attempts. Check authentication or network. Service stopping."
        echo "ERROR: Initial pull failed after $MAX_PULL_RETRIES attempts. Exiting."
        exit 0  # Exit cleanly so systemd doesn't restart
      fi

      echo "Retrying in $CURRENT_DELAY seconds..."
      sleep "$CURRENT_DELAY"
      CURRENT_DELAY=$((CURRENT_DELAY * 2))
    done

    LAST_PULL=$(date +%s)

    echo "Watching $REPO_DIR for changes (debounce: ''${DEBOUNCE_SECONDS}s, pull interval: ''${PULL_INTERVAL_SECONDS}s)"

    # Watch for file changes, debounce, then sync
    ${pkgs.inotify-tools}/bin/inotifywait \
      -m -r \
      -e modify,create,delete,move \
      --exclude '/\.git/' \
      --format '%w%f' \
      "$REPO_DIR" |
    while read -t "$PULL_INTERVAL_SECONDS" CHANGED_FILE || true; do
      NOW=$(date +%s)

      if [ -n "''${CHANGED_FILE:-}" ]; then
        # File changed — debounce by consuming events for DEBOUNCE_SECONDS
        echo "Change detected: $CHANGED_FILE"
        while read -t "$DEBOUNCE_SECONDS" EXTRA_FILE; do
          echo "  also changed: $EXTRA_FILE"
        done
        commit_and_push
        LAST_PULL=$NOW
      fi

      # Periodic pull if enough time has passed
      ELAPSED=$((NOW - LAST_PULL))
      if [ "$ELAPSED" -ge "$PULL_INTERVAL_SECONDS" ]; then
        pull_remote
        LAST_PULL=$(date +%s)
      fi
    done
  '';

  repoOpts = { name, ... }: {
    options = {
      path = lib.mkOption {
        type = lib.types.str;
        description = "Absolute path to the git repository.";
        example = "/home/user/notes";
      };

      branch = lib.mkOption {
        type = lib.types.str;
        default = "main";
        description = "Git branch to sync.";
      };

      debounceSeconds = lib.mkOption {
        type = lib.types.int;
        default = 5;
        description = "Seconds to wait after last file change before committing.";
      };

      pullIntervalSeconds = lib.mkOption {
        type = lib.types.int;
        default = 30;
        description = "Seconds between periodic pulls from remote.";
      };

      sshKey = lib.mkOption {
        type = lib.types.nullOr lib.types.str;
        default = null;
        description = "Path to SSH private key for git push/pull. If null, uses the user's default SSH config.";
        example = "/home/alice/.ssh/id_ed25519";
      };

      gitName = lib.mkOption {
        type = lib.types.str;
        default = "autosync";
        description = "Git author name for auto-commits.";
      };

      gitEmail = lib.mkOption {
        type = lib.types.str;
        default = "autosync@localhost";
        description = "Git author email for auto-commits.";
      };

      maxPullRetries = lib.mkOption {
        type = lib.types.int;
        default = 6;
        description = "Maximum number of pull retries before giving up and stopping the service.";
      };

      retryDelaySeconds = lib.mkOption {
        type = lib.types.int;
        default = 4;
        description = "Seconds to wait between pull retry attempts, increases exponentially.";
      };
    };
  };

in {
  options.services.git-autosync = {
    enable = lib.mkEnableOption "git-autosync file watcher and sync service";

    repos = lib.mkOption {
      type = lib.types.attrsOf (lib.types.submodule repoOpts);
      default = {};
      description = "Set of git repositories to auto-sync.";
      example = {
        notes = {
          path = "/home/alice/notes";
          branch = "main";
        };
      };
    };
  };

  config = lib.mkIf cfg.enable {
    systemd.user.services = lib.mapAttrs' (name: repo:
      lib.nameValuePair "git-autosync-${name}" {
        Unit = {
          Description = "Git auto-sync for ${repo.path}";
          After = [ "network-online.target" ];
          Wants = [ "network-online.target" ];
        };

        Service = {
          Type = "simple";
          Restart = "on-failure";
          RestartSec = 10;

          ExecStart = "${git-autosync-script} ${repo.path} ${toString repo.debounceSeconds} ${toString repo.pullIntervalSeconds} ${repo.branch} ${toString repo.maxPullRetries} ${toString repo.retryDelaySeconds}";

          Environment = [
            "GIT_AUTHOR_NAME=${repo.gitName}"
            "GIT_COMMITTER_NAME=${repo.gitName}"
            "GIT_AUTHOR_EMAIL=${repo.gitEmail}"
            "GIT_COMMITTER_EMAIL=${repo.gitEmail}"
          ] ++ lib.optionals (repo.sshKey != null) [
            "GIT_SSH_COMMAND=ssh -i ${repo.sshKey} -o StrictHostKeyChecking=accept-new"
          ];
        };

        Install = {
          WantedBy = [ "default.target" ];
        };
      }
    ) cfg.repos;
  };
}
