'use strict';

const { EventEmitter } = require('node:events');
const fs = require('node:fs');
const path = require('node:path');
const crypto = require('node:crypto');
const { WorkspaceReplay } = require('./workspace-replay');
const { CodingSessionIndex } = require('./session-index');

class AcpAdapter {
  constructor({
    replay = null,
    permissionService = null,
    eventBus = null,
    runner = null,
    runAgentLoop = null,
    sessionIndex = null,
  } = {}) {
    this.replay = replay || new WorkspaceReplay();
    this.sessionIndex = sessionIndex || new CodingSessionIndex({ replay: this.replay, sessionsDir: this.replay.sessionsDir });
    this.permissionService = permissionService || null;
    this.eventBus = eventBus || new EventEmitter();
    this.runner = runner || null;
    this.runAgentLoop = runAgentLoop || null;
    this.sessions = new Map();
    this.subscribers = new Map();
    this.history = new Map();

    this.eventBus.on('permission.asked', (entry) => {
      this._emit(entry.sessionId || '', 'permission', { state: 'asked', request: entry });
    });
    this.eventBus.on('permission.replied', (entry) => {
      this._emit(entry.sessionId || '', 'permission', { state: 'replied', request: entry });
    });
  }

  async createSession({ sessionId = crypto.randomUUID(), cwd = process.cwd(), prompt = '', model = '', agent = 'build', mode = 'build', run = false } = {}) {
    this.sessions.set(sessionId, { sessionId, cwd, model, agent, mode, status: run ? 'running' : 'created' });
    this.replay.appendRecord(sessionId, {
      type: 'session_meta',
      cwd,
      modelId: model,
      label: prompt.slice(0, 80),
      agent,
      mode,
    });
    if (prompt) {
      this.replay.appendRecord(sessionId, {
        type: 'user',
        cwd,
        message: { role: 'user', content: [{ type: 'text', text: prompt }] },
      });
    }
    this._emit(sessionId, 'session.created', { sessionId, cwd, model, agent, mode });

    if (run) {
      const result = await this._runSession({ sessionId, cwd, prompt, model, agent, mode });
      this.sessions.set(sessionId, { ...this.sessions.get(sessionId), status: result.success === false ? 'failed' : 'completed' });
      return { sessionId, cwd, model, agent, mode, result };
    }
    return { sessionId, cwd, model, agent, mode };
  }

  async promptSession({ sessionId, prompt = '', cwd = '', model = '', agent = 'build', mode = 'build' } = {}) {
    if (!sessionId) throw new Error('sessionId is required');
    const previous = this.sessions.get(sessionId) || {};
    const next = {
      sessionId,
      cwd: cwd || previous.cwd || process.cwd(),
      model: model || previous.model || '',
      agent: agent || previous.agent || 'build',
      mode: mode || previous.mode || 'build',
      status: 'running',
    };
    this.sessions.set(sessionId, next);
    if (prompt) {
      this.replay.appendRecord(sessionId, {
        type: 'user',
        cwd: next.cwd,
        message: { role: 'user', content: [{ type: 'text', text: prompt }] },
      });
      this._emit(sessionId, 'message.user', { text: prompt });
    }
    const result = await this._runSession({
      sessionId,
      cwd: next.cwd,
      prompt,
      model: next.model,
      agent: next.agent,
      mode: next.mode,
    });
    const stopReason = result?.cancelled ? 'cancelled' : result?.success === false ? 'refusal' : 'end_turn';
    this.sessions.set(sessionId, {
      ...next,
      status: stopReason === 'end_turn' ? 'completed' : stopReason,
    });
    this._emit(sessionId, 'session.prompt.complete', { stopReason, result });
    return { stopReason, result };
  }

  loadSession(sessionId, opts = {}) {
    const state = this.replay.rebuildState(sessionId, opts);
    const existing = this.sessions.get(sessionId) || {};
    this.sessions.set(sessionId, {
      sessionId,
      cwd: opts.cwd || state.cwd || existing.cwd || '',
      model: state.meta.modelId || existing.model || '',
      agent: state.meta.agent || existing.agent || '',
      mode: state.meta.mode || existing.mode || '',
      status: existing.status || 'loaded',
    });
    return state;
  }

  listSessions(opts = {}) {
    return this.sessionIndex.listSessions(opts);
  }

  setSessionOptions(sessionId, updates = {}) {
    const previous = this.sessions.get(sessionId) || { sessionId };
    const next = { ...previous, ...updates, sessionId };
    this.sessions.set(sessionId, next);
    this.replay.appendPart(sessionId, 'session_options', updates, { cwd: next.cwd || '' });
    this._emit(sessionId, 'session.options', next);
    return next;
  }

  cancelSession(sessionId, reason = 'cancelled') {
    const previous = this.sessions.get(sessionId) || { sessionId };
    const next = { ...previous, status: 'cancelled', cancelReason: reason };
    this.sessions.set(sessionId, next);
    this.replay.appendPart(sessionId, 'cancelled', { reason }, { cwd: next.cwd || '' });
    this._emit(sessionId, 'session.cancelled', { sessionId, reason });
    return next;
  }

  subscribe(sessionId, { offset = 0, sequence = 0, onEvent } = {}) {
    if (typeof onEvent !== 'function') throw new Error('onEvent callback is required');
    const replay = this.replay.replayRange(sessionId, { offset, sequence });
    for (const entry of replay.records) {
      onEvent({ type: 'transcript', sessionId, sequence: entry.sequence, offset: entry.offset, record: entry.record });
    }
    for (const evt of this.history.get(sessionId) || []) {
      onEvent(evt);
    }
    for (const request of this.listPermissions({ sessionId })) {
      onEvent({ type: 'permission', sessionId, data: { state: 'pending', request } });
    }

    const set = this.subscribers.get(sessionId) || new Set();
    set.add(onEvent);
    this.subscribers.set(sessionId, set);
    return () => {
      const current = this.subscribers.get(sessionId);
      if (!current) return;
      current.delete(onEvent);
      if (current.size === 0) this.subscribers.delete(sessionId);
    };
  }

  listPermissions({ sessionId = '' } = {}) {
    if (!this.permissionService?.list) return [];
    return this.permissionService.list({ sessionId });
  }

  replyPermission(requestId, reply = 'once', message = '') {
    if (!this.permissionService?.reply) return false;
    return this.permissionService.reply({ requestId, reply, message });
  }

  writeFilesFromAcceptedDiff({ sessionId = '', workspaceRoot, files = [] } = {}) {
    const root = realpathBestEffort(workspaceRoot);
    if (!root) throw new Error('workspaceRoot is required');
    const written = [];
    for (const file of files) {
      const target = resolveInside(root, file.path || file.filePath || file.name || '');
      fs.mkdirSync(path.dirname(target), { recursive: true });
      fs.writeFileSync(target, String(file.content ?? ''), 'utf8');
      const rel = path.relative(root, target);
      written.push(rel);
    }
    if (sessionId) {
      this.replay.appendPart(sessionId, 'accepted_diff', { files: written }, { cwd: root });
      this._emit(sessionId, 'files.written', { files: written, workspaceRoot: root });
    }
    return { workspaceRoot: root, files: written };
  }

  async _runSession({ sessionId, cwd, prompt, model, agent, mode }) {
    if (this.runner) return this.runner({ sessionId, cwd, prompt, model, agent, mode, adapter: this });
    if (!this.runAgentLoop) return { success: true, deferred: true };
    return this.runAgentLoop(prompt, {
      cwd,
      model,
      mode,
      runSessionId: sessionId,
    });
  }

  _emit(sessionId, type, data = {}) {
    if (!sessionId) return;
    const event = { type, sessionId, data, timestamp: new Date().toISOString() };
    const history = this.history.get(sessionId) || [];
    history.push(event);
    if (history.length > 200) history.shift();
    this.history.set(sessionId, history);
    const subscribers = this.subscribers.get(sessionId);
    if (subscribers) {
      for (const send of subscribers) send(event);
    }
  }
}

function resolveInside(root, requestedPath) {
  if (!requestedPath) throw new Error('file path is required');
  const target = path.resolve(root, requestedPath);
  const rel = path.relative(root, target);
  if (rel.startsWith('..') || path.isAbsolute(rel)) {
    throw new Error(`Path escapes workspace: ${requestedPath}`);
  }
  return target;
}

function realpathBestEffort(p) {
  if (!p) return '';
  try { return fs.realpathSync(p); } catch { return path.resolve(p); }
}

module.exports = {
  AcpAdapter,
  resolveInside,
};
