/**
 * @file Preflight checks before destructive lane operations (reset, remove, purge).
 * Queries the current state without mutations: git status, unpushed commits, sessions
 * to be purged, and blockers (adopted lanes, missing directories, unpushed work).
 * Every result is read-only; the route and action layer decide what to do with blocks.
 * @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
 */

const fs = require("node:fs");
const { db } = require("../db");
const wt = require("./worktree");
const lanesLib = require("./lanes");
const { resolveProfile } = require("./lane-profile");
const { dbName } = require("./lane-slots");

/**
 * Preflight for reset, remove, or purge. Returns an object describing what will happen:
 * - reset/remove: {action, lane, kind, branch, dirty, untracked, unpushed, head, blocked, warnings}
 * - purge: {action, lane, sessions, events, tokenRows, bytesEstimate, activeSessionSkipped}
 *
 * blocked[] holds only conditions that genuinely prevent the action: "adopted" (not
 * managed), "missing" (dir gone), "unreadable" (git failed against the directory), and
 * "unpushed-commits" (unpushed > 0 — the only one a `force: true` overrides). warnings[]
 * holds purely informational facts that never gate the action, starting with "no-remote"
 * (no git remote configured — nothing is backed up, but the action proceeds). Both are
 * data, not an exception.
 *
 * @param {object} lane - The lane to check
 * @param {string} action - One of "reset", "remove", "purge"
 * @returns {Promise<object>} Preflight report
 */
async function preflight(lane, action) {
  const blocked = [];
  const warnings = [];

  // Check if lane is adopted (not managed)
  if (lane.kind === "adopted") {
    blocked.push("adopted");
  }

  // For reset/remove, return git status and blockers
  if (action === "reset" || action === "remove") {
    let dirty = 0;
    let untracked = 0;
    let unpushed = 0;
    let head = null;

    // First check if directory exists
    if (!fs.existsSync(lane.cwd)) {
      blocked.push("missing");
    } else {
      // Directory exists, try to read git status
      try {
        // Get status counts
        const status = await wt.statusCounts(lane.cwd);
        dirty = status.dirty;
        untracked = status.untracked;
        head = status.head;

        // Get unpushed count — measured against the lane's own base branch when
        // there is no remote, so it counts this lane's work, not the whole repo.
        unpushed = await wt.unpushedCount(lane.cwd, lane.base_branch);
        if (unpushed > 0) {
          blocked.push("unpushed-commits");
        }

        // Detect no remotes configured at all — informational only, never a blocker:
        // a perfectly ordinary local-only managed lane has no remote at all.
        const noRemote = await wt.hasNoRemotes(lane.cwd);
        if (noRemote) {
          warnings.push("no-remote");
        }
      } catch (err) {
        // Directory exists but git failed (corrupt repo, permission denied, etc.)
        blocked.push("unreadable");
      }
    }

    // The database name this action would drop (reset unless --keep-db,
    // remove always) — echoed the same way `head`/`dirty` are, so the
    // confirmation dialog names the destructive fact rather than leaving it a
    // surprise. Null when the lane has no slot yet or the profile declares no
    // DB_PREFIX — nothing has been derived to drop.
    const profile = resolveProfile(lane);
    const database = profile && lane.slot ? dbName(profile, lane.slot) : null;

    return {
      action,
      lane: lane.id,
      kind: lane.kind,
      branch: lane.branch,
      dirty,
      untracked,
      unpushed,
      head: head || null,
      database,
      blocked,
      warnings,
    };
  }

  // For purge, count sessions and events to be deleted
  if (action === "purge") {
    const counts = countPurgeSessions(lane);

    return {
      action,
      lane: lane.id,
      sessions: counts.sessions,
      events: counts.events,
      tokenRows: counts.tokenRows,
      bytesEstimate: (counts.events + counts.tokenRows) * 512,
      activeSessionSkipped: counts.activeSessionSkipped,
    };
  }

  // Unknown action should never reach here (route validates)
  throw Object.assign(new Error(`unknown action: ${action}`), { code: "EBADACTION" });
}

/**
 * Count sessions, events, and token_usage rows that would be deleted by purgeLaneSessions.
 * Uses the shared purgeCandidateSessions helper to ensure the counts match exactly what
 * gets deleted, so the confirmation dialog's numbers are truthful.
 */
function countPurgeSessions(lane) {
  const result = { sessions: 0, events: 0, tokenRows: 0, activeSessionSkipped: false };

  // Use the shared helpers so the path matching (and its LIKE escaping) has
  // exactly one definition, in server/lib/lanes.js.
  const sessionsToDelete = lanesLib.purgeCandidateSessions(lane);
  result.activeSessionSkipped = lanesLib.hasActiveLaneSession(lane);

  if (sessionsToDelete.length === 0) {
    return result;
  }

  // Count events for those sessions
  const sessionIds = sessionsToDelete.map((s) => s.id);
  const eventsCount = db
    .prepare(
      `SELECT COUNT(*) as count FROM events WHERE session_id IN (${sessionIds.map(() => "?").join(",")})`
    )
    .get(...sessionIds);
  result.events = eventsCount ? eventsCount.count : 0;

  // Count orphaned token_usage rows
  const tokenCount = db
    .prepare(
      `SELECT COUNT(*) as count FROM token_usage WHERE session_id IN (${sessionIds.map(() => "?").join(",")})`
    )
    .get(...sessionIds);
  result.tokenRows = tokenCount ? tokenCount.count : 0;

  // Count sessions (for completeness)
  result.sessions = sessionsToDelete.length;

  return result;
}

module.exports = { preflight };
