'use strict';

/**
 * state-watcher.js
 *
 * Locates the WoW installation, watches every account's RockyCoreWoWDeckHelper
 * SavedVariables file, parses it, and emits the freshest state.
 * Replaces the old standalone bridge process.
 */

const fs = require('fs');
const path = require('path');
const { EventEmitter } = require('events');
const { execFileSync } = require('child_process');
const chokidar = require('chokidar');
const luaparse = require('luaparse');

const SAVED_VARS_FILENAME = 'RockyCoreWoWDeckHelper.lua';

// ── WoW install detection ────────────────────────────────────────────────────
function candidateInstallDirs() {
  const candidates = [];

  if (process.platform === 'win32') {
    // Registry: Blizzard writes InstallPath under the uninstall key
    try {
      const out = execFileSync('reg', [
        'query',
        'HKLM\\SOFTWARE\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\World of Warcraft',
        '/v', 'InstallLocation',
      ], { encoding: 'utf8', windowsHide: true });
      const m = out.match(/InstallLocation\s+REG_SZ\s+(.+)/);
      if (m) candidates.push(m[1].trim());
    } catch { /* key absent — fall through to path guessing */ }

    const roots = ['C:', 'D:', 'E:', 'F:'];
    for (const root of roots) {
      candidates.push(`${root}\\Program Files (x86)\\World of Warcraft`);
      candidates.push(`${root}\\Program Files\\World of Warcraft`);
      candidates.push(`${root}\\World of Warcraft`);
      candidates.push(`${root}\\Games\\World of Warcraft`);
    }
  } else if (process.platform === 'darwin') {
    candidates.push('/Applications/World of Warcraft');
  }

  return candidates;
}

/**
 * Resolve the _retail_ directory. `override` may point at the WoW root,
 * the _retail_ dir, or anything containing one of them.
 */
function findRetailDir(override) {
  const tryDirs = override ? [override] : candidateInstallDirs();

  for (const dir of tryDirs) {
    if (!dir) continue;
    for (const probe of [dir, path.join(dir, '_retail_')]) {
      if (fs.existsSync(path.join(probe, 'WTF', 'Account'))) {
        return probe;
      }
    }
  }
  return null;
}

/** All SavedVariables files for this addon across every account folder. */
function findSavedVarsFiles(retailDir) {
  const accountRoot = path.join(retailDir, 'WTF', 'Account');
  let accounts;
  try {
    accounts = fs.readdirSync(accountRoot, { withFileTypes: true });
  } catch {
    return [];
  }

  const files = [];
  for (const entry of accounts) {
    if (!entry.isDirectory()) continue;
    const file = path.join(accountRoot, entry.name, 'SavedVariables', SAVED_VARS_FILENAME);
    if (fs.existsSync(file)) files.push(file);
  }
  return files;
}

// ── Lua SavedVariables parsing (ported from the old bridge) ──────────────────
function parseLuaSavedVars(luaText) {
  const ast = luaparse.parse(luaText, { luaVersion: '5.1' });

  const assignment = ast.body.find(node =>
    node.type === 'AssignmentStatement' &&
    node.variables[0]?.name === 'RockyCoreWoWDeckHelper_State'
  );

  if (!assignment) throw new Error('RockyCoreWoWDeckHelper_State not found');
  return luaTableToJS(assignment.init[0]);
}

function luaTableToJS(node) {
  if (!node) return null;

  if (node.type === 'TableConstructorExpression') {
    const obj = {};
    let arrayIndex = 1;

    for (const field of node.fields) {
      let key, value;

      if (field.type === 'TableKeyString') {
        key = field.key.name || field.key.value || stripQuotes(field.key.raw);
        value = luaTableToJS(field.value);
      } else if (field.type === 'TableKey') {
        const k = field.key;
        if (k.type === 'StringLiteral') key = k.value ?? stripQuotes(k.raw);
        else if (k.type === 'NumericLiteral') key = k.value;
        else key = luaTableToJS(k);
        value = luaTableToJS(field.value);
      } else if (field.type === 'TableValue') {
        key = arrayIndex++;
        value = luaTableToJS(field.value);
      }

      if (key !== undefined && key !== null) {
        obj[key] = value;
      }
    }
    return obj;
  }

  if (node.type === 'NumericLiteral') return node.value;
  if (node.type === 'StringLiteral') return node.value ?? stripQuotes(node.raw);
  if (node.type === 'BooleanLiteral') return node.value;
  if (node.type === 'NilLiteral') return null;
  if (node.type === 'UnaryExpression' && node.operator === '-') {
    return -luaTableToJS(node.argument);
  }

  console.warn('[StateWatcher] Unknown AST node type:', node.type);
  return null;
}

function stripQuotes(raw) {
  if (!raw) return null;
  return raw.replace(/^["']|["']$/g, '');
}

// ── Watcher ──────────────────────────────────────────────────────────────────
/**
 * Emits:
 *   'state'   (state, filePath) — parsed state newer than anything seen so far
 *   'status'  (string)          — human-readable status for logging
 *
 * When several accounts have the addon, the file with the newest
 * state.timestamp wins.
 */
class StateWatcher extends EventEmitter {
  constructor({ wowPathOverride = null } = {}) {
    super();
    this.wowPathOverride = wowPathOverride;
    this.currentState = null;
    this.lastTimestamp = 0;
    this.watcher = null;
    this.retailDir = null;
    this.rescanTimer = null;
  }

  start() {
    this.retailDir = findRetailDir(this.wowPathOverride);

    if (!this.retailDir) {
      this.emit('status',
        'WoW installation not found. Set a WoW folder override in the button settings.');
      // Retry: WoW may be installed later, or override may arrive
      this.rescanTimer = setTimeout(() => this.start(), 30_000);
      return;
    }

    const accountRoot = path.join(this.retailDir, 'WTF', 'Account');
    this.emit('status',
      `Watching: ${accountRoot}\\*\\SavedVariables\\${SAVED_VARS_FILENAME}`);

    // Load whatever exists right now
    for (const file of findSavedVarsFiles(this.retailDir)) {
      this.loadFile(file);
    }

    // Watch the account root as a literal path rather than a glob: a glob
    // silently matches nothing when the install path contains parentheses
    // (e.g. "Program Files (x86)"), which picomatch reads as an extglob group.
    // depth 2 reaches <account>/SavedVariables/<file> and picks up account
    // folders created later.
    this.watcher = chokidar.watch(accountRoot, {
      persistent: true,
      ignoreInitial: true,
      disableGlobbing: true,
      depth: 2,
      awaitWriteFinish: {
        stabilityThreshold: 300,
        pollInterval: 100,
      },
    });

    const onFile = (file) => {
      if (path.basename(file) === SAVED_VARS_FILENAME) this.loadFile(file);
    };
    this.watcher.on('add', onFile);
    this.watcher.on('change', onFile);
    this.watcher.on('error', (err) => {
      this.emit('status', `Watcher error: ${err.message}`);
    });
  }

  setWowPathOverride(newPath) {
    if ((newPath || null) === this.wowPathOverride) return;
    this.wowPathOverride = newPath || null;
    this.stop();
    this.start();
  }

  loadFile(filePath) {
    let raw;
    try {
      raw = fs.readFileSync(filePath, 'utf8');
    } catch (e) {
      this.emit('status', `Could not read ${filePath}: ${e.message}`);
      return;
    }

    let parsed;
    try {
      parsed = parseLuaSavedVars(raw);
    } catch (e) {
      this.emit('status', `Parse error in ${filePath}: ${e.message}`);
      return;
    }

    if (!parsed || typeof parsed.timestamp !== 'number') return;
    if (parsed.timestamp <= this.lastTimestamp) return; // stale or duplicate event

    this.lastTimestamp = parsed.timestamp;
    this.currentState = parsed;

    const slotCount = Object.keys(parsed.slots || {}).length;
    this.emit('status',
      `State updated — class: ${parsed.meta?.playerClass}, ` +
      `spec: ${parsed.meta?.specName}, slots: ${slotCount}`);
    this.emit('state', parsed, filePath);
  }

  stop() {
    if (this.rescanTimer) {
      clearTimeout(this.rescanTimer);
      this.rescanTimer = null;
    }
    if (this.watcher) {
      this.watcher.close();
      this.watcher = null;
    }
  }
}

module.exports = { StateWatcher, findRetailDir, parseLuaSavedVars };
