'use strict';

const { execSync } = require('node:child_process');
const path = require('node:path');
const { createLogger } = require('./logger');

const log = createLogger('git');

const CACHE_TTL = 120000; // 2 minutes — short TTLs cause frequent event-loop blocking
const _cache = new Map();

/**
 * Execute a git command in a directory with timeout.
 * @param {string} command - Git command to run
 * @param {string} cwd - Working directory
 * @param {number} [timeout=5000] - Timeout in ms
 * @returns {string} - stdout output
 */
function _exec(command, cwd, timeout = 5000) {
  return execSync(command, {
    cwd,
    timeout,
    encoding: 'utf8',
    stdio: ['pipe', 'pipe', 'pipe']
  }).trim();
}

/**
 * Check if a directory is a git repository.
 * @param {string} dir - Directory path
 * @returns {boolean}
 */
function isGitRepo(dir) {
  try {
    _exec('git rev-parse --is-inside-work-tree', dir);
    return true;
  } catch {
    return false;
  }
}

/**
 * Get git info for a project directory. Results are cached with a 2-minute TTL.
 * @param {string} dir - Project directory path
 * @returns {{ branch: string, dirty: boolean, lastCommit: string, lastCommitAge: string, latestTag: string|null }|null}
 */
function getInfo(dir) {
  const cacheKey = path.resolve(dir);
  const cached = _cache.get(cacheKey);
  if (cached && Date.now() - cached.time < CACHE_TTL) {
    return cached.data;
  }

  const data = _fetchInfo(dir);
  _cache.set(cacheKey, { data, time: Date.now() });
  return data;
}

/**
 * Fetch git info without caching.
 * @param {string} dir - Project directory path
 * @returns {{ branch: string, dirty: boolean, lastCommit: string, lastCommitAge: string, latestTag: string|null }|null}
 */
function _fetchInfo(dir) {
  if (!isGitRepo(dir)) return null;

  try {
    const branch = _getBranch(dir);
    const dirty = _isDirty(dir);

    // Check if the repo has any commits before running log/describe
    let hasCommits = true;
    try {
      _exec('git rev-parse HEAD', dir);
    } catch {
      hasCommits = false;
    }

    const lastCommit = hasCommits ? _getLastCommitMessage(dir) : '';
    const lastCommitAge = hasCommits ? _getLastCommitAge(dir) : '';
    const latestTag = hasCommits ? _getLatestTag(dir) : null;

    return { branch, dirty, lastCommit, lastCommitAge, latestTag };
  } catch (err) {
    log.warn('Failed to get git info', { dir, error: err.message });
    return null;
  }
}

/**
 * Get the current branch name.
 * @param {string} dir - Repository directory
 * @returns {string}
 */
function _getBranch(dir) {
  try {
    return _exec('git rev-parse --abbrev-ref HEAD', dir);
  } catch {
    return 'unknown';
  }
}

/**
 * Check if the working directory has uncommitted changes.
 * @param {string} dir - Repository directory
 * @returns {boolean}
 */
function _isDirty(dir) {
  try {
    const output = _exec('git status --porcelain', dir);
    return output.length > 0;
  } catch {
    return false;
  }
}

/**
 * Get the latest reachable tag (e.g. "v3.0.0").
 * @param {string} dir - Repository directory
 * @returns {string|null}
 */
function _getLatestTag(dir) {
  try {
    return _exec('git describe --tags --abbrev=0', dir);
  } catch {
    return null;
  }
}

/**
 * Get the last commit message (first line).
 * @param {string} dir - Repository directory
 * @returns {string}
 */
function _getLastCommitMessage(dir) {
  try {
    return _exec('git log -1 --format=%s', dir);
  } catch {
    return '';
  }
}

/**
 * Get the relative age of the last commit.
 * @param {string} dir - Repository directory
 * @returns {string}
 */
function _getLastCommitAge(dir) {
  try {
    return _exec('git log -1 --format=%cr', dir);
  } catch {
    return '';
  }
}

/**
 * Check if the working directory has uncommitted changes.
 * @param {string} dir - Repository directory
 * @returns {boolean}
 */
function isDirty(dir) {
  return _isDirty(dir);
}

/**
 * Stage all changes and commit with the given message.
 * @param {string} dir - Repository directory
 * @param {string} message - Commit message
 * @returns {{ committed: boolean, error?: string }}
 */
function commit(dir, message) {
  try {
    if (!_isDirty(dir)) {
      return { committed: false };
    }
    _exec('git add -A', dir, 10000);
    // Escape single quotes in commit message
    const escaped = message.replace(/'/g, "'\\''");
    _exec(`git commit -m '${escaped}'`, dir, 15000);
    log.info('Auto-committed changes', { dir, message });
    clearCacheFor(dir);
    return { committed: true };
  } catch (err) {
    log.warn('Auto-commit failed', { dir, error: err.message });
    return { committed: false, error: err.message };
  }
}

/**
 * Clear the git info cache. Useful for testing or forcing refresh.
 */
function clearCache() {
  _cache.clear();
}

/**
 * Clear cache for a specific directory.
 * @param {string} dir - Directory path to clear
 */
function clearCacheFor(dir) {
  _cache.delete(path.resolve(dir));
}

module.exports = {
  isGitRepo,
  isDirty,
  commit,
  getInfo,
  clearCache,
  clearCacheFor,
  _exec,
  _fetchInfo
};
