export interface SessionMeta {
  paneId: string;
  sessionId: string;
  projectPath: string;
  title?: string;
  description?: string;
  parentAgent?: string;
  createdAt: Date;
  lastActivityAt: Date;
  timeoutSeconds?: number;
  idleExpiresAt?: Date;
  absoluteExpiresAt?: Date;
  timedOut?: boolean;
}

const sessions = new Map<string, SessionMeta>();

export interface RegisterOpts {
  timeoutSeconds?: number;
  absoluteTimeoutSeconds?: number;
  description?: string;
  parentAgent?: string;
}

export function register(
  sessionId: string,
  paneId: string,
  projectPath: string,
  title?: string,
  opts?: RegisterOpts,
): void {
  const now = new Date();
  const existing = sessions.get(paneId);
  if (existing) {
    existing.lastActivityAt = now;
    return;
  }
  const idleExpiresAt = opts?.timeoutSeconds && opts.timeoutSeconds > 0
    ? new Date(now.getTime() + opts.timeoutSeconds * 1000)
    : undefined;
  const absoluteExpiresAt = opts?.absoluteTimeoutSeconds && opts.absoluteTimeoutSeconds > 0
    ? new Date(now.getTime() + opts.absoluteTimeoutSeconds * 1000)
    : undefined;
  sessions.set(paneId, {
    paneId,
    sessionId,
    projectPath,
    title,
    description: opts?.description,
    parentAgent: opts?.parentAgent,
    createdAt: now,
    lastActivityAt: now,
    timeoutSeconds: opts?.timeoutSeconds,
    idleExpiresAt,
    absoluteExpiresAt,
  });
}

export function unregister(paneId: string): void {
  sessions.delete(paneId);
  ringBuffers.delete(paneId);
}

/**
 * Bump the lastActivityAt timestamp for a pane.
 * Called on every PTY data write so the idle-timeout sweep knows when a session
 * was last active.
 */
export function touchActivity(paneId: string): void {
  const meta = sessions.get(paneId);
  if (meta) {
    meta.lastActivityAt = new Date();
  }
}

export function list(): SessionMeta[] {
  return Array.from(sessions.values());
}

export function get(paneId: string): SessionMeta | undefined {
  return sessions.get(paneId);
}

// ── Pending metadata (POST /start → WS attach handoff) ──────────────────────
//
// The POST /start route stores optional description/parentAgent here; the WS
// attach handler consumes it when calling register(). This avoids coupling the
// HTTP route to the WS lifecycle while keeping the handoff single-process and
// ephemeral (no DB writes).

const pendingMetadata = new Map<string, { description?: string; parentAgent?: string }>();

export function setPendingMetadata(
  paneId: string,
  meta: { description?: string; parentAgent?: string },
): void {
  pendingMetadata.set(paneId, meta);
}

export function consumePendingMetadata(
  paneId: string,
): { description?: string; parentAgent?: string } | undefined {
  const meta = pendingMetadata.get(paneId);
  if (meta) pendingMetadata.delete(paneId);
  return meta;
}

// ── Ring buffer for PTY output search ──────────────────────────────────────

export interface SearchMatch {
  line: number;
  content: string;
  contextBefore: string[];
  contextAfter: string[];
}

const ringBuffers = new Map<string, string[]>();

/**
 * Return the last N non-empty lines from the ring buffer for a pane.
 * ANSI escape sequences are preserved (xterm handles them).
 * Partial lines from mid-stream exit are included as-is.
 */
export function getLastLines(paneId: string, n: number): string[] {
  const buf = ringBuffers.get(paneId);
  if (!buf || buf.length === 0) return [];
  const nonEmpty = buf.filter(l => l.trim().length > 0);
  return nonEmpty.slice(-n);
}

/**
 * Append raw PTY data to the ring buffer for a given pane.
 * Splits incoming data on newlines and pushes each line into the buffer,
 * trimming to `maxLines` (default 5000) from the tail.
 */
export function appendOutput(
  paneId: string,
  data: string,
  maxLines: number = 5000,
): void {
  let buf = ringBuffers.get(paneId);
  if (!buf) {
    buf = [];
    ringBuffers.set(paneId, buf);
  }

  // Split on newlines — each chunk may contain multiple complete lines and
  // potentially a trailing partial line (which we store as-is; the next chunk
  // will either complete it or be another partial).
  const lines = data.split('\n');

  // The first element of `lines` may be a continuation of the last partial
  // line from the previous append. If the buffer is non-empty and the last
  // stored entry is a partial (no trailing newline previously), glue them.
  // We detect "partial" by checking whether `data` ended with '\n' — if it
  // did, the last element after split is '' (empty) which we drop.
  const endedWithNewline = data.endsWith('\n');
  if (endedWithNewline) {
    // The final empty-string element is discarded.
    lines.pop();
  }

  if (buf.length > 0 && lines.length > 0) {
    // Concatenate the last partial line in the buffer with the first split
    // segment. This avoids splitting ANSI sequences or text across chunks.
    buf[buf.length - 1] = (buf[buf.length - 1] ?? '') + (lines[0] ?? '');
    lines.shift();
  }

  for (const line of lines) {
    buf.push(line);
  }

  // Trim from head if over maxLines
  if (buf.length > maxLines) {
    buf = buf.slice(buf.length - maxLines);
    ringBuffers.set(paneId, buf);
  }
}

/**
 * Search the ring buffer for a pane using a regex pattern.
 * Returns matches with optional context lines before and after each match.
 */
export function searchRingBuffer(
  paneId: string,
  pattern: string,
  opts?: { limit?: number; context?: number },
): SearchMatch[] {
  const buf = ringBuffers.get(paneId);
  if (!buf || buf.length === 0) return [];

  const limit = opts?.limit ?? 50;
  const context = opts?.context ?? 0;

  let re: RegExp;
  try {
    re = new RegExp(pattern, 'u');
  } catch {
    return []; // invalid regex — caller should validate, but be defensive
  }

  const results: SearchMatch[] = [];

  for (let i = 0; i < buf.length; i++) {
    if (results.length >= limit) break;
    if (re.test(buf[i]!)) {
      const contextBefore: string[] = [];
      const contextAfter: string[] = [];
      for (let c = 1; c <= context; c++) {
        const ci = i - c;
        if (ci >= 0) contextBefore.unshift(buf[ci]!);
      }
      for (let c = 1; c <= context; c++) {
        const ci = i + c;
        if (ci < buf.length) contextAfter.push(buf[ci]!);
      }
      results.push({
        line: i + 1, // 1-based line number for display
        content: buf[i]!,
        contextBefore,
        contextAfter,
      });
    }
  }

  return results;
}

/**
 * Remove the ring buffer for a pane. Called on session kill / pane close.
 */
export function clearBuffer(paneId: string): void {
  ringBuffers.delete(paneId);
}

/**
 * Return all sessions whose idle-expiry or absolute-expiry has passed.
 * A session with no timeout configured is never included.
 * Called by the sweepExpired interval in manager.ts.
 */
export function getTimedOutSessions(): SessionMeta[] {
  const now = Date.now();
  const result: SessionMeta[] = [];
  for (const meta of sessions.values()) {
    const idleHit = meta.idleExpiresAt && now >= meta.idleExpiresAt.getTime();
    const absoluteHit = meta.absoluteExpiresAt && now >= meta.absoluteExpiresAt.getTime();
    if (idleHit || absoluteHit) {
      result.push(meta);
    }
  }
  return result;
}
