import { promises as fs } from 'fs';
import path from 'path';
import { sendCronDaemonRequest } from './services/cron-daemon-owner.js';
import { getAlwaysOnHeartbeatPath } from './services/always-on-paths.js';

function normalizeString(value) {
  return typeof value === 'string' ? value.trim() : '';
}

async function resolveProjectRoot(project) {
  const direct = normalizeString(project?.projectRoot) || normalizeString(project?.fullPath) || normalizeString(project?.path);
  if (direct) return path.resolve(direct);
  const projectName = normalizeString(project?.projectName) || normalizeString(project?.name);
  if (!projectName) return '';
  const { extractProjectDirectory } = await import('./projects.js');
  return path.resolve(await extractProjectDirectory(projectName));
}

async function resolveProjectRoots(projects) {
  if (!Array.isArray(projects)) return [];
  const roots = [];
  for (const project of projects) {
    const root = await resolveProjectRoot(project);
    if (root) roots.push(root);
  }
  return [...new Set(roots)];
}

async function registerProject(projectRoot) {
  try {
    await sendCronDaemonRequest({ type: 'register_project', projectRoot });
  } catch {
    // The daemon may not be up yet; the next heartbeat will retry.
  }
}

export function createAlwaysOnHeartbeatManager({
  getActivePilotDeckSessions = () => [],
  registerProjectFn = registerProject
} = {}) {
  const wsFiles = new WeakMap();
  const wsIds = new WeakMap();
  const wsProjectRoots = new WeakMap();

  function getWsId(ws) {
    let id = wsIds.get(ws);
    if (!id) {
      id = `${Date.now()}-${Math.random().toString(16).slice(2)}`;
      wsIds.set(ws, id);
    }
    return id;
  }

  async function writeBeat(ws, projectRoot, payload) {
    const wsId = getWsId(ws);
    const fileName = `webui-${wsId}.beat`;
    const filePath = getAlwaysOnHeartbeatPath(projectRoot, fileName);
    const processingSessionIds = Array.isArray(payload.processingSessionIds)
      ? payload.processingSessionIds.filter((id) => typeof id === 'string')
      : [];
    const beat = {
      schemaVersion: 1,
      writerKind: 'webui',
      writerId: wsId,
      writtenAt: new Date().toISOString(),
      agentBusy: Boolean(payload.agentBusy) || processingSessionIds.length > 0,
      processingSessionIds,
      lastUserMsgAt: normalizeString(payload.lastUserMsgAt) || null,
    };

    await fs.mkdir(path.dirname(filePath), { recursive: true });
    await fs.writeFile(filePath, JSON.stringify(beat, null, 2), 'utf8');
    const files = wsFiles.get(ws) || new Set();
    files.add(filePath);
    wsFiles.set(ws, files);
    await registerProjectFn(projectRoot);
  }

  async function handlePresence(ws, payload = {}) {
    const roots = new Map();
    const selectedRoot = await resolveProjectRoot(payload.selectedProject);
    const alwaysOnRoots = await resolveProjectRoots(payload.alwaysOnProjects);

    for (const projectRoot of alwaysOnRoots) {
      roots.set(projectRoot, {
        agentBusy: false,
        processingSessionIds: [],
        lastUserMsgAt: selectedRoot === projectRoot ? payload.lastUserMsgAt : null,
      });
    }
    const activeSessions = getActivePilotDeckSessions();
    for (const session of activeSessions) {
      const cwd = normalizeString(session?.cwd);
      if (!cwd) continue;
      const projectRoot = path.resolve(cwd);
      const existing = roots.get(projectRoot);
      if (!existing) continue;
      existing.agentBusy = true;
      existing.processingSessionIds.push(session.sessionId);
      roots.set(projectRoot, existing);
    }

    for (const [projectRoot, beatPayload] of roots) {
      await writeBeat(ws, projectRoot, beatPayload);
    }
    wsProjectRoots.set(ws, new Set(roots.keys()));
    return [...roots.keys()];
  }

  async function clearPresence(ws) {
    const files = wsFiles.get(ws);
    if (!files) {
      wsProjectRoots.delete(ws);
      return;
    }
    await Promise.all([...files].map((filePath) => fs.rm(filePath, { force: true }).catch(() => {})));
    wsFiles.delete(ws);
    wsProjectRoots.delete(ws);
  }

  return {
    getWriterId: getWsId,
    getProjectRoots(ws) {
      return [...(wsProjectRoots.get(ws) || [])];
    },
    handlePresence,
    clearPresence,
  };
}