const fs = require('fs');
const path = require('path');
const { spawn, spawnSync } = require('child_process');

const { activeConnection, configDir, configPath, defaultMemoryApiUrl, defaultWorldModelApiUrl, packageRoot, readConfig } = require('./config');
const { ensureFreshConnection } = require('./auth');
const { portalEnsureFresh } = require('./portal-auth');
const { getBinary } = require('./cielara');

function tokenFilePath() {
  return path.join(configDir(), 'mcp-token');
}

function writeTokenFile(token) {
  const filePath = tokenFilePath();
  fs.mkdirSync(path.dirname(filePath), { recursive: true, mode: 0o700 });
  fs.writeFileSync(filePath, token, { encoding: 'utf8', mode: 0o600 });
  if (process.platform === 'win32') {
    // 0o600 is ignored on Windows — restrict via icacls to current user only.
    // Grant Modify (M), not Read (R): the server rewrites this file on token
    // refresh, so read-only ACL causes EPERM on the next open-for-write.
    spawnSync('icacls', [filePath, '/inheritance:r', '/grant:r', `${process.env.USERNAME}:M`], { stdio: 'ignore' });
  }
}

const PYTHON_ENV_VERSION = '3';

function pythonDir() {
  // Dev convenience: run straight from the sibling core checkout if present.
  const mcpServerDir = path.join(packageRoot(), '..', 'core', 'mcp-server');
  if (fs.existsSync(path.join(mcpServerDir, 'memory_mcp', 'server.py'))) {
    return mcpServerDir;
  }
  return path.join(packageRoot(), 'python');
}

function venvDir() {
  return path.join(configDir(), 'connector-python');
}

function venvPython() {
  return process.platform === 'win32'
    ? path.join(venvDir(), 'Scripts', 'python.exe')
    : path.join(venvDir(), 'bin', 'python');
}

function markerPath() {
  return path.join(venvDir(), '.cielara-env-version');
}

function commandExists(command) {
  const result = spawnSync(command, ['--version'], { stdio: 'ignore' });
  return result.status === 0;
}

function supportsPythonVersion(command) {
  const result = spawnSync(command, ['-c', 'import sys; raise SystemExit(0 if sys.version_info >= (3, 11) else 1)'], {
    stdio: 'ignore',
  });
  return result.status === 0;
}

function findPython() {
  if (
    process.env.CIELARA_PYTHON
    && commandExists(process.env.CIELARA_PYTHON)
    && supportsPythonVersion(process.env.CIELARA_PYTHON)
  ) {
    return process.env.CIELARA_PYTHON;
  }
  for (const candidate of ['python3', 'python']) {
    if (commandExists(candidate) && supportsPythonVersion(candidate)) {
      return candidate;
    }
  }
  throw new Error('Python 3.11+ is required. Install Python or set CIELARA_PYTHON.');
}

function runLogged(command, args, options = {}) {
  return new Promise((resolve, reject) => {
    const child = spawn(command, args, {
      ...options,
      stdio: ['ignore', 'pipe', 'pipe'],
    });
    child.stdout.on('data', (chunk) => process.stderr.write(chunk));
    child.stderr.on('data', (chunk) => process.stderr.write(chunk));
    child.on('error', reject);
    child.on('close', (code) => {
      if (code === 0) {
        resolve();
      } else {
        reject(new Error(`${command} ${args.join(' ')} exited with ${code}`));
      }
    });
  });
}

function envReady() {
  try {
    return fs.existsSync(venvPython())
      && fs.readFileSync(markerPath(), 'utf8').trim() === PYTHON_ENV_VERSION;
  } catch (_) {
    return false;
  }
}

async function ensurePythonEnv() {
  if (envReady()) {
    return venvPython();
  }

  fs.mkdirSync(configDir(), { recursive: true, mode: 0o700 });
  const python = findPython();
  process.stderr.write(`Preparing Cielara Connector Python environment in ${venvDir()}\n`);
  await runLogged(python, ['-m', 'venv', venvDir()]);
  await runLogged(venvPython(), ['-m', 'pip', 'install', '--upgrade', 'pip']);
  const reqFile = path.join(packageRoot(), 'python', 'requirements.txt');
  const altReqFile = path.join(pythonDir(), 'requirements.txt');
  await runLogged(venvPython(), ['-m', 'pip', 'install', '-r', fs.existsSync(reqFile) ? reqFile : altReqFile]);
  fs.writeFileSync(markerPath(), `${PYTHON_ENV_VERSION}\n`);
  return venvPython();
}

// For portal connections, ensureFreshConnection() fails because connector.json
// has no refreshToken — tokens live in the OS keychain. Read the raw connection
// for URL metadata, then get the token via the correct path for each auth type.
async function resolveConnection() {
  const config = readConfig();
  const { name, connection: raw } = activeConnection(config);
  if (raw.portalUrl) {
    // Portal: URLs are in connector.json, token comes from keychain
    const { credentials } = await portalEnsureFresh(name);
    return { connection: raw, accessToken: credentials.accessToken || '' };
  }
  // Legacy OAuth: ensureFreshConnection refreshes the token if needed
  const { connection } = await ensureFreshConnection();
  return { connection, accessToken: connection.accessToken || '' };
}

// The memory MCP server and the cielara-code workload-topology daemon are two
// separate processes. Clients (Codex, Claude Desktop, …) auto-start this memory
// server at launch, but nothing warms the topology daemon until the first
// `cielara-code` invocation — on Codex, deliberately hookless at SessionStart,
// that first contact is the user's first /askcielara query, which connects
// before the daemon has loaded its topology (warm-up race → "no schema loaded").
// Fire a detached, best-effort warm here so the daemon is loading its topology
// from the moment the MCP server boots, well before the first question. Spawning
// any non-skip `cielara-code` command runs its maybe_warm_daemon() at startup;
// `workloads ls` is cheap and not on the warm skip-list.
function warmSidecarDaemon() {
  try {
    const bin = getBinary('cielara-code');
    if (!bin) return; // git checkout without compiled binaries — nothing to warm
    const child = spawn(bin, ['workloads', 'ls'], {
      detached: true,
      stdio: 'ignore',
    });
    child.on('error', () => {}); // never let a warm failure touch MCP startup
    child.unref();
  } catch (_) {
    // best-effort only
  }
}

async function runMCP() {
  const { connection, accessToken } = await resolveConnection();
  writeTokenFile(accessToken);
  startRefreshLoop();
  warmSidecarDaemon();

  const baseUrl = (connection.backendUrl || connection.baseUrl || connection.portalUrl || '').replace(/\/$/, '');
  const memoryApiUrl = connection.memoryApiUrl || defaultMemoryApiUrl(baseUrl);
  let worldModelUrl;
  if (Object.prototype.hasOwnProperty.call(connection, 'worldModelUrl')) {
    worldModelUrl = connection.worldModelUrl ? String(connection.worldModelUrl).replace(/\/$/, '') : '';
  } else {
    worldModelUrl = defaultWorldModelApiUrl(baseUrl);
  }
  const env = {
    ...process.env,
    CIELARA_CONNECTOR_CONFIG: configPath(),
    CIELARA_BACKEND_URL: baseUrl,
    MEMORY_API_URL: memoryApiUrl,
    CIELARA_MCP_TOKEN_FILE: tokenFilePath(),
  };
  if (worldModelUrl) {
    env.WORLD_MODEL_URL = worldModelUrl;
  }

  const python = await ensurePythonEnv();
  env.PYTHONPATH = [pythonDir(), process.env.PYTHONPATH].filter(Boolean).join(path.delimiter);
  const child = spawn(python, ['-m', 'memory_mcp'], { cwd: pythonDir(), env, stdio: 'inherit' });

  child.on('error', (error) => {
    process.stderr.write(`Failed to start Cielara MCP server: ${error.message}\n`);
    process.exit(1);
  });
  child.on('close', (code, signal) => {
    if (signal) {
      process.kill(process.pid, signal);
      return;
    }
    process.exit(code || 0);
  });
}

function startRefreshLoop() {
  const refresh = async () => {
    try {
      const { accessToken } = await resolveConnection();
      if (accessToken) {
        writeTokenFile(accessToken);
      }
    } catch (error) {
      process.stderr.write(`Cielara token refresh failed: ${error.message}\n`);
    }
  };
  const timer = setInterval(refresh, 60 * 1000);
  timer.unref();
}

module.exports = {
  ensurePythonEnv,
  runMCP,
};
