'use strict';

/**
 * Unified phone/remote-access connection health.
 *
 * Produces ONE normalized health record per method (Microsoft Dev Tunnel, Walle Remote
 * relay, Tailscale) so the Access page can show a single status truth, detect
 * expiry/offline, fire an action-required notification on the TRANSITION to broken, and
 * auto-recover when the user fixes it.
 *
 * Probes are READ-ONLY (no recovery side-effects) — the existing Microsoft watchdog owns
 * actually fixing the tunnel; this module reports state and offers `action` endpoints the
 * client triggers. Cheap enough to poll (~20s) — no deep public-reachability probe here.
 *
 * Record shape:
 *   { method, state, detail, origin, action, since, checked_at }
 *   state: 'ok' | 'warning' | 'expired' | 'offline' | 'error' | 'not_configured'
 *   action: null | { kind, endpoint, method }   // a one-click fix the client can POST/GET
 */

const fs = require('fs');
const { X509Certificate } = require('crypto');

const microsoft = require('./microsoft-dev-tunnel-setup');
const tailscale = require('./tailscale-setup');
const remoteRelay = require('./remote-relay-store');

// Walle relay heartbeat: if not connected and last handshake older than this, treat as offline.
const WALLE_STALE_MS = Number(process.env.CTM_WALLE_RELAY_STALE_MS) || 5 * 60 * 1000;
const CERT_SOON_MS = 24 * 60 * 60 * 1000;

// States that count as "broken" (drive the Reconnect badge + action-required notification).
const BROKEN = new Set(['expired', 'offline', 'error']);

// method -> { state, since } — transition cache (also keeps `since` accurate between polls).
const _stateCache = new Map();

function _rec(method, { state, detail, origin = '', action = null }) {
  return { method, state, detail, origin, action };
}

// How far past its scheduled renewal the managed tunnel is (ms; 0 = on track).
// Considers both the tunnel renewal and the anonymous-access renewal.
function _renewalOverdueMs(managed, now = Date.now()) {
  let worst = 0;
  for (const key of ['next_tunnel_renewal_at', 'next_access_renewal_at']) {
    const at = Date.parse(managed && managed[key] || '');
    if (Number.isFinite(at) && now > at) worst = Math.max(worst, now - at);
  }
  return worst;
}

async function probeMicrosoftHealth(opts) {
  try {
    const d = await microsoft.detectMicrosoftDevTunnelSetup(opts);
    const managed = d.managed_tunnel || {};
    const configured = !!(managed.configured || managed.desired_running || managed.tunnel_id);
    if (!d.installed) {
      return _rec('microsoft', { state: 'not_configured', detail: 'devtunnel CLI is not installed.',
        action: { kind: 'install', endpoint: '/api/setup/network/microsoft-dev-tunnel/install', method: 'POST' } });
    }
    if (!d.signed_in) {
      // Signed out: if a tunnel was previously configured this is almost always an EXPIRED login.
      return _rec('microsoft', configured
        ? { state: 'expired', detail: 'Microsoft sign-in expired — reconnect.',
            action: { kind: 'login', endpoint: '/api/setup/network/microsoft-dev-tunnel/login', method: 'POST' } }
        : { state: 'not_configured', detail: 'Not signed in.',
            action: { kind: 'login', endpoint: '/api/setup/network/microsoft-dev-tunnel/login', method: 'POST' } });
    }
    if (managed.desired_running && !managed.running) {
      return _rec('microsoft', { state: 'offline', detail: 'Tunnel should be running but is not — reconnect.', origin: d.mobile_url,
        action: { kind: 'recover', endpoint: '/api/setup/network/microsoft-dev-tunnel/recover', method: 'POST' } });
    }
    if (!configured || !managed.running) {
      return _rec('microsoft', { state: 'not_configured', detail: 'Tunnel not started yet.', origin: d.mobile_url,
        action: { kind: 'start', endpoint: '/api/setup/network/microsoft-dev-tunnel/start', method: 'POST' } });
    }
    if (managed.access && managed.access.status === 'error') {
      return _rec('microsoft', { state: 'warning', detail: managed.access_error || 'Tunnel access needs attention.', origin: d.mobile_url,
        action: { kind: 'recover', endpoint: '/api/setup/network/microsoft-dev-tunnel/recover', method: 'POST' } });
    }
    // Renewal runs at day 25 of the 30-day Dev Tunnels expiration. If it is
    // overdue the tunnel is burning its ~5-day runway toward expiry — and an
    // expired tunnel gets RECREATED with a NEW URL, which invalidates every
    // paired phone (cookies + passkeys are hostname-bound). Escalate before that:
    // >24h overdue = warning, >3 days = broken (fires the action-required alert).
    const overdue = _renewalOverdueMs(managed);
    if (overdue > 3 * 24 * 60 * 60 * 1000) {
      return _rec('microsoft', { state: 'error', detail: 'Tunnel renewal has been failing for days — the URL will expire and rotate. Reconnect.', origin: d.mobile_url,
        action: { kind: 'recover', endpoint: '/api/setup/network/microsoft-dev-tunnel/recover', method: 'POST' } });
    }
    if (overdue > 24 * 60 * 60 * 1000) {
      return _rec('microsoft', { state: 'warning', detail: 'Tunnel renewal is overdue — reconnect before the URL expires and rotates.', origin: d.mobile_url,
        action: { kind: 'recover', endpoint: '/api/setup/network/microsoft-dev-tunnel/recover', method: 'POST' } });
    }
    return _rec('microsoft', { state: 'ok', detail: 'Connected.', origin: d.mobile_url });
  } catch (err) {
    return _rec('microsoft', { state: 'error', detail: String(err?.message || err).slice(0, 200) });
  }
}

async function probeWalleHealth(db) {
  try {
    if (!db) return _rec('walle', { state: 'not_configured', detail: 'Relay store unavailable.' });
    remoteRelay.ensureRemoteRelaySchema(db);
    const s = remoteRelay.buildRemoteStatus(db);
    if (!s.configured) return _rec('walle', { state: 'not_configured', detail: 'Walle Remote is not set up.' });
    if (s.connected) return _rec('walle', { state: 'ok', detail: 'Relay connected.', origin: s.relay && s.relay.mobile_url });
    // Configured but not connected. Only treat as a BREAK if it was connected before
    // (last_connected_at exists) — otherwise it's a default instance that was never used,
    // which must NOT raise a false "offline — reconnect" alert.
    const last = Number(s.instance && s.instance.last_connected_at) || 0;
    if (!last) return _rec('walle', { state: 'not_configured', detail: 'Set up Walle Remote to pair a phone.', origin: s.relay && s.relay.mobile_url });
    const stale = (Date.now() - last) > WALLE_STALE_MS;
    return _rec('walle', {
      state: 'offline',
      detail: stale ? 'Relay is not connected.' : 'Relay reconnecting…',
      origin: s.relay && s.relay.mobile_url,
      action: null, // no programmatic re-dial yet (relay client auto-reconnects); surface guidance
    });
  } catch (err) {
    return _rec('walle', { state: 'error', detail: String(err?.message || err).slice(0, 200) });
  }
}

// Returns { expired, soon, notAfter } or null if cert can't be read/parsed.
function _certStatus(certPath) {
  try {
    if (!certPath || !fs.existsSync(certPath)) return null;
    const x = new X509Certificate(fs.readFileSync(certPath));
    const notAfter = new Date(x.validTo).getTime();
    if (!Number.isFinite(notAfter)) return null;
    const now = Date.now();
    return { expired: now > notAfter, soon: notAfter - now < CERT_SOON_MS, notAfter };
  } catch {
    return null;
  }
}

async function probeTailscaleHealth(opts) {
  try {
    const t = await tailscale.detectTailscaleSetup(opts);
    const tailscaleAuto = { kind: 'recover', endpoint: '/api/setup/network/tailscale-auto', method: 'POST' };
    if (!t.installed) return _rec('tailscale', { state: 'not_configured', detail: 'Tailscale is not installed.' });
    // "Set up for CTM phone access" = an HTTPS cert was generated. Without it, Tailscale
    // may be installed/used for other things but isn't a CTM phone path — so a stopped/
    // logged-out node must NOT raise a false "offline — reconnect" alert.
    if (!t.cert_exists) {
      return _rec('tailscale', {
        state: 'not_configured',
        detail: t.available ? 'Run auto setup to enable HTTPS for phones.' : 'Tailscale is not set up for phone access.',
        origin: t.mobile_url, action: tailscaleAuto,
      });
    }
    // Cert exists → the user set this up. Now report real liveness (authoritative
    // BackendState + cert validity), so a genuine break surfaces as offline/expired.
    const cert = _certStatus(t.cert_path);
    if (cert && cert.expired) {
      return _rec('tailscale', { state: 'expired', detail: 'HTTPS certificate expired — regenerate.', origin: t.mobile_url, action: tailscaleAuto });
    }
    const backend = String(t.backend_state || '');
    if (backend === 'NeedsLogin') {
      return _rec('tailscale', { state: 'expired', detail: 'Tailscale needs sign-in — run “tailscale up”.', origin: t.mobile_url, action: tailscaleAuto });
    }
    if (!t.available || (backend && backend !== 'Running')) {
      const detail = backend === 'Stopped' ? 'Tailscale is stopped — run “tailscale up”.'
        : backend === 'Starting' ? 'Tailscale is starting…'
        : 'Tailscale is not connected — run “tailscale up”.';
      return _rec('tailscale', { state: backend === 'Starting' ? 'warning' : 'offline', detail, origin: t.mobile_url, action: tailscaleAuto });
    }
    if (cert && cert.soon) {
      return _rec('tailscale', { state: 'warning', detail: 'HTTPS certificate expires soon.', origin: t.mobile_url, action: tailscaleAuto });
    }
    return _rec('tailscale', { state: 'ok', detail: 'Reachable on your tailnet.', origin: t.mobile_url });
  } catch (err) {
    return _rec('tailscale', { state: 'error', detail: String(err?.message || err).slice(0, 200) });
  }
}

// Update each record's `since`/`checked_at` from the cache and return broken-edge transitions.
function _applyTransitions(records, nowIso) {
  const transitions = [];
  for (const r of records) {
    const prev = _stateCache.get(r.method);
    if (!prev || prev.state !== r.state) {
      r.since = nowIso;
      if (prev && !BROKEN.has(prev.state) && BROKEN.has(r.state)) {
        transitions.push({ method: r.method, from: prev.state, to: r.state });
      }
      _stateCache.set(r.method, { state: r.state, since: nowIso });
    } else {
      r.since = prev.since;
    }
    r.checked_at = nowIso;
  }
  return transitions;
}

const _ORDER = { microsoft: 0, walle: 1, tailscale: 2 };
function _pickRecommended(records) {
  const oks = records.filter((r) => r.state === 'ok');
  if (oks.length >= 1) {
    return oks.sort((a, b) => _ORDER[a.method] - _ORDER[b.method])[0].method;
  }
  return 'microsoft'; // simplest to set up (no VPN app, no custom domain)
}

async function getConnectionHealth(deps = {}) {
  const opts = { configDir: deps.configDir, port: deps.port };
  const nowIso = new Date().toISOString();
  const order = ['microsoft', 'walle', 'tailscale'];
  const settled = await Promise.allSettled([
    probeMicrosoftHealth(opts),
    probeWalleHealth(deps.db),
    probeTailscaleHealth(opts),
  ]);
  const records = settled.map((s, i) => (s.status === 'fulfilled'
    ? s.value
    : _rec(order[i], { state: 'error', detail: String(s.reason && s.reason.message || s.reason).slice(0, 200) })));
  const transitions = _applyTransitions(records, nowIso);
  return { ok: true, checked_at: nowIso, recommended: _pickRecommended(records), methods: records, transitions };
}

module.exports = {
  getConnectionHealth,
  probeMicrosoftHealth,
  probeWalleHealth,
  probeTailscaleHealth,
  BROKEN,
  _stateCache,
  _internal: { _certStatus, _applyTransitions, _pickRecommended, _renewalOverdueMs },
};
