/**
 * Persistent command history store for terminal autocomplete.
 * Stores commands per host with frequency tracking and timestamp ordering.
 * Uses localStorageAdapter as the persistence layer (works in renderer process).
 */

import { localStorageAdapter } from "../../../infrastructure/persistence/localStorageAdapter";

const STORAGE_KEY = "netcatty:commandHistory";
const MAX_ENTRIES = 10000;
const MAX_ENTRIES_PER_HOST = 5000;

export interface HistoryEntry {
  command: string;
  hostId: string;
  /** OS type for cross-host matching */
  os: "linux" | "windows" | "macos";
  /** Number of times this exact command was executed */
  frequency: number;
  /** Timestamp of last execution */
  lastUsedAt: number;
  /** Timestamp of first execution */
  createdAt: number;
}

interface HistoryStore {
  entries: HistoryEntry[];
  version: number;
}

let cachedStore: HistoryStore | null = null;

function loadStore(): HistoryStore {
  if (cachedStore) return cachedStore;
  try {
    const parsed = localStorageAdapter.read<HistoryStore>(STORAGE_KEY);
    if (parsed) {
      cachedStore = parsed;
      return parsed;
    }
  } catch {
    // Corrupted data, reset
  }
  cachedStore = { entries: [], version: 1 };
  return cachedStore;
}

let saveTimer: ReturnType<typeof setTimeout> | null = null;

function saveStore(store: HistoryStore): void {
  cachedStore = store;
  // Debounce saves to avoid excessive writes
  if (saveTimer) clearTimeout(saveTimer);
  saveTimer = setTimeout(() => {
    const ok = localStorageAdapter.write(STORAGE_KEY, store);
    if (!ok) {
      // Storage full — evict lowest scored entries (not just oldest by insertion)
      const now = Date.now();
      store.entries.sort((a, b) => scoreEntryAt(b, now) - scoreEntryAt(a, now));
      store.entries = store.entries.slice(0, Math.floor(MAX_ENTRIES / 2));
      localStorageAdapter.write(STORAGE_KEY, store);
    }
    saveTimer = null;
  }, 500);
}

/**
 * Record a command execution. Updates frequency if the command already exists
 * for this host, otherwise creates a new entry.
 */
export function recordCommand(
  command: string,
  hostId: string,
  os: "linux" | "windows" | "macos" = "linux",
): void {
  const trimmed = command.trim();
  if (!trimmed || trimmed.length > 2000) return;

  const store = loadStore();
  const now = Date.now();

  // Find existing entry for same command + host
  const existingIdx = store.entries.findIndex(
    (e) => e.command === trimmed && e.hostId === hostId,
  );

  if (existingIdx >= 0) {
    store.entries[existingIdx].frequency++;
    store.entries[existingIdx].lastUsedAt = now;
  } else {
    store.entries.push({
      command: trimmed,
      hostId,
      os,
      frequency: 1,
      lastUsedAt: now,
      createdAt: now,
    });
  }

  // Enforce per-host limit (evict by score, not insertion order)
  const hostEntries = store.entries.filter((e) => e.hostId === hostId);
  if (hostEntries.length > MAX_ENTRIES_PER_HOST) {
    hostEntries.sort((a, b) => scoreEntryAt(a, now) - scoreEntryAt(b, now));
    const toRemove = new Set(
      hostEntries.slice(0, hostEntries.length - MAX_ENTRIES_PER_HOST).map((e) => e.command),
    );
    store.entries = store.entries.filter(
      (e) => e.hostId !== hostId || !toRemove.has(e.command),
    );
  }

  // Enforce global limit
  if (store.entries.length > MAX_ENTRIES) {
    store.entries.sort((a, b) => scoreEntryAt(b, now) - scoreEntryAt(a, now));
    store.entries = store.entries.slice(0, MAX_ENTRIES);
  }

  saveStore(store);
}

/**
 * Score an entry for ranking at a specific timestamp.
 * Caches Date.now() at query boundaries to avoid repeated syscalls during sort.
 */
function scoreEntryAt(entry: HistoryEntry, now: number): number {
  const ageMs = now - entry.lastUsedAt;
  const ageHours = ageMs / (1000 * 60 * 60);
  // Exponential decay: halve relevance every 24 hours
  const recencyScore = Math.pow(0.5, ageHours / 24);
  return entry.frequency * recencyScore;
}

export interface HistoryQueryOptions {
  /** Filter by host ID (strict isolation — only this host's history) */
  hostId?: string;
  /** Maximum number of results */
  limit?: number;
}

export interface RecentHistoryQueryOptions extends HistoryQueryOptions {
  /** Base command name, e.g. `cd` or `ls` */
  commandName: string;
  /** Exact command text to exclude from results */
  excludeCommand?: string;
  /** Optional path prefix to require on the current argument */
  argumentPrefix?: string;
}

/**
 * Query history entries matching a prefix.
 * Returns entries sorted by relevance (frequency * recency).
 */
export function queryHistory(
  prefix: string,
  options: HistoryQueryOptions = {},
): HistoryEntry[] {
  const { hostId, limit = 20 } = options;
  if (limit <= 0) return [];
  const store = loadStore();
  const lowerPrefix = prefix.toLowerCase();
  const now = Date.now(); // Cache once per query

  const filtered = store.entries.filter((entry) => {
    // Must match prefix
    if (!entry.command.toLowerCase().startsWith(lowerPrefix)) return false;
    // Must not be identical to prefix
    if (entry.command === prefix) return false;

    // Host filtering: strict per-host isolation
    if (hostId) {
      return entry.hostId === hostId;
    }
    return true;
  });

  // Sort by score (frequency * recency)
  filtered.sort((a, b) => scoreEntryAt(b, now) - scoreEntryAt(a, now));

  // Deduplicate by command text (keep highest scored)
  const seen = new Set<string>();
  const results: HistoryEntry[] = [];
  for (const entry of filtered) {
    if (seen.has(entry.command)) continue;
    seen.add(entry.command);
    results.push(entry);
    if (results.length >= limit) break;
  }

  return results;
}

/**
 * Fuzzy query: matches commands containing all characters of the query
 * in order (not necessarily contiguous). Used as a fallback when prefix
 * matching yields few results.
 */
export function fuzzyQueryHistory(
  query: string,
  options: HistoryQueryOptions = {},
): HistoryEntry[] {
  const { hostId, limit = 10 } = options;
  if (limit <= 0) return [];
  const store = loadStore();
  const lowerQuery = query.toLowerCase();
  const now = Date.now(); // Cache once per query

  const scored: { entry: HistoryEntry; matchScore: number }[] = [];

  for (const entry of store.entries) {
    // Host filtering
    if (hostId) {
      if (entry.hostId !== hostId) continue;
    }

    const matchScore = fuzzyScore(lowerQuery, entry.command.toLowerCase());
    if (matchScore > 0 && entry.command !== query) {
      scored.push({ entry, matchScore });
    }
  }

  scored.sort((a, b) =>
    b.matchScore * scoreEntryAt(b.entry, now) - a.matchScore * scoreEntryAt(a.entry, now),
  );

  const seen = new Set<string>();
  const results: HistoryEntry[] = [];
  for (const { entry } of scored) {
    if (seen.has(entry.command)) continue;
    seen.add(entry.command);
    results.push(entry);
    if (results.length >= limit) break;
  }

  return results;
}

/**
 * Query the most recently used history entries for the same command name.
 * Useful when the user is currently completing a path argument and wants
 * a few recent command-line examples (e.g. recent `cd ...` commands).
 */
export function queryRecentHistoryByCommand(
  options: RecentHistoryQueryOptions,
): HistoryEntry[] {
  const {
    commandName,
    excludeCommand,
    argumentPrefix,
    hostId,
    limit = 3,
  } = options;
  if (!commandName || limit <= 0) return [];

  const store = loadStore();
  const trimmedCommandName = commandName.trim().toLowerCase();
  const commandPrefix = `${trimmedCommandName} `;
  const normalizedArgumentPrefix = normalizeArgumentToken(argumentPrefix ?? "");

  const filtered = store.entries.filter((entry) => {
    const lowerCommand = entry.command.toLowerCase();
    if (lowerCommand !== trimmedCommandName && !lowerCommand.startsWith(commandPrefix)) {
      return false;
    }
    if (excludeCommand && entry.command === excludeCommand) return false;

    if (normalizedArgumentPrefix) {
      const currentToken = normalizeArgumentToken(getCurrentCommandToken(entry.command));
      if (!currentToken.startsWith(normalizedArgumentPrefix)) {
        return false;
      }
    }

    if (hostId) {
      return entry.hostId === hostId;
    }
    return true;
  });

  filtered.sort((a, b) => b.lastUsedAt - a.lastUsedAt);

  const seen = new Set<string>();
  const results: HistoryEntry[] = [];
  for (const entry of filtered) {
    if (seen.has(entry.command)) continue;
    seen.add(entry.command);
    results.push(entry);
    if (results.length >= limit) break;
  }

  return results;
}

function getCurrentCommandToken(command: string): string {
  const tokens = tokenizeShellLike(command);
  return tokens.length > 0 ? (tokens[tokens.length - 1] || "") : "";
}

function normalizeArgumentToken(token: string): string {
  return token
    .trim()
    .replace(/^['"]/, "")
    .replace(/['"]$/, "")
    .replace(/\\ /g, " ")
    .toLowerCase();
}

function tokenizeShellLike(input: string): string[] {
  const tokens: string[] = [];
  let current = "";
  let inSingleQuote = false;
  let inDoubleQuote = false;
  let escaped = false;

  for (let i = 0; i < input.length; i++) {
    const ch = input[i];

    if (escaped) {
      current += ch;
      escaped = false;
      continue;
    }

    if (ch === "\\") {
      escaped = true;
      current += ch;
      continue;
    }

    if (ch === "'" && !inDoubleQuote) {
      inSingleQuote = !inSingleQuote;
      current += ch;
      continue;
    }

    if (ch === '"' && !inSingleQuote) {
      inDoubleQuote = !inDoubleQuote;
      current += ch;
      continue;
    }

    if (ch === " " && !inSingleQuote && !inDoubleQuote) {
      if (current.length > 0) {
        tokens.push(current);
        current = "";
      }
      continue;
    }

    current += ch;
  }

  tokens.push(current);
  return tokens;
}

/**
 * Compute a fuzzy match score. Returns 0 for no match.
 * Higher score = better match quality.
 * Rewards: first-char match, consecutive matches, word-boundary matches.
 */
function fuzzyScore(query: string, target: string): number {
  if (query.length === 0) return 0;
  if (query.length > target.length) return 0;

  let score = 0;
  let queryIdx = 0;
  let prevMatchIdx = -2;

  for (let i = 0; i < target.length && queryIdx < query.length; i++) {
    if (target[i] === query[queryIdx]) {
      queryIdx++;
      // First character bonus
      if (i === 0) score += 10;
      // Consecutive match bonus
      if (i === prevMatchIdx + 1) score += 5;
      // Word boundary bonus
      if (i === 0 || target[i - 1] === " " || target[i - 1] === "/" ||
          target[i - 1] === "-" || target[i - 1] === "_") {
        score += 3;
      }
      score += 1;
      prevMatchIdx = i;
    }
  }

  // All query characters must be matched
  return queryIdx === query.length ? score : 0;
}

/**
 * Clear all history for a specific host, or all history if no hostId given.
 */
export function clearHistory(hostId?: string): void {
  const store = loadStore();
  if (hostId) {
    store.entries = store.entries.filter((e) => e.hostId !== hostId);
  } else {
    store.entries = [];
  }
  saveStore(store);
}
