import type { PluginInput } from '@opencode-ai/plugin';
import { POLL_INTERVAL_BACKGROUND_MS } from '../config';
import type { MultiplexerConfig } from '../config/schema';
import {
  getMultiplexer,
  isServerRunning,
  type Multiplexer,
} from '../multiplexer';
import { log } from '../utils/logger';

type OpencodeClient = PluginInput['client'];

interface TrackedSession {
  sessionId: string;
  paneId: string;
  parentId: string;
  title: string;
  directory: string;
  createdAt: number;
  lastSeenAt: number;
  hasSeenBusy: boolean;
  idleSince?: number;
  missingSince?: number;
}

interface KnownSession {
  parentId: string;
  title: string;
  directory: string;
}

interface SessionEvent {
  type: string;
  properties?: {
    info?: {
      id?: string;
      parentID?: string;
      title?: string;
      directory?: string;
    };
    sessionID?: string;
    status?: { type: string };
  };
}

type CloseReason = 'idle' | 'deleted' | 'missing' | 'timeout';

const SESSION_TIMEOUT_MS = 10 * 60 * 1000;
const SESSION_IDLE_GRACE_MS = 15 * 1000;
const SESSION_IDLE_DEBOUNCE_MS = POLL_INTERVAL_BACKGROUND_MS * 2;
const SESSION_MISSING_GRACE_MS = POLL_INTERVAL_BACKGROUND_MS * 3;

/**
 * Tracks child sessions and spawns/closes multiplexer panes for them.
 *
 * Uses session.status events for completion detection instead of polling,
 * with polling kept as a fallback for reliability.
 */
export class MultiplexerSessionManager {
  private static activeControllerKey: string | null = null;
  private static hasActiveController = false;

  private client: OpencodeClient;
  private serverUrl: string;
  private directory: string;
  private multiplexer: Multiplexer | null = null;
  private sessions = new Map<string, TrackedSession>();
  private knownSessions = new Map<string, KnownSession>();
  private spawningSessions = new Set<string>();
  private pendingBusySessions = new Set<string>();
  private pendingIdleSessions = new Map<string, number>();
  private closingSessions = new Map<string, Promise<void>>();
  private pollInterval?: ReturnType<typeof setInterval>;
  private enabled = false;

  constructor(ctx: PluginInput, config: MultiplexerConfig) {
    this.client = ctx.client;
    this.directory = ctx.directory;
    const defaultPort = process.env.OPENCODE_PORT ?? '4096';
    this.serverUrl =
      ctx.serverUrl?.toString() ?? `http://localhost:${defaultPort}`;

    this.multiplexer = getMultiplexer(config);
    const controllerKey = this.getControllerKey(config.type);
    let isController = false;
    if (controllerKey !== null) {
      if (!MultiplexerSessionManager.hasActiveController) {
        MultiplexerSessionManager.activeControllerKey = controllerKey;
        MultiplexerSessionManager.hasActiveController = true;
        isController = true;
      } else {
        isController = false;
      }
    }

    this.enabled =
      process.env.OMOS_MULTIPLEXER_CHILD !== '1' &&
      config.type !== 'none' &&
      this.multiplexer?.isInsideSession() === true &&
      isController;

    log('[multiplexer-session-manager] initialized', {
      enabled: this.enabled,
      type: config.type,
      serverUrl: this.serverUrl,
      controllerKey,
      activeControllerKey: MultiplexerSessionManager.activeControllerKey,
    });
  }

  private getControllerKey(type: MultiplexerConfig['type']): string | null {
    if (type === 'tmux') {
      return process.env.TMUX_PANE ?? null;
    }

    if (type === 'zellij') {
      return process.env.ZELLIJ_PANE_ID ?? process.env.ZELLIJ ?? null;
    }

    return null;
  }

  async onSessionCreated(event: SessionEvent): Promise<void> {
    if (!this.enabled || !this.multiplexer) return;
    if (event.type !== 'session.created') return;

    const info = event.properties?.info;
    if (!info?.id || !info?.parentID) {
      return;
    }

    const sessionId = info.id;
    const parentId = info.parentID;
    const title = info.title ?? 'Subagent';
    const directory = info.directory ?? this.directory;

    if (this.isTrackedOrSpawning(sessionId)) {
      log('[multiplexer-session-manager] session already tracked or spawning', {
        sessionId,
      });
      return;
    }

    if (this.knownSessions.has(sessionId)) {
      this.knownSessions.set(sessionId, {
        parentId,
        title,
        directory,
      });
      log('[multiplexer-session-manager] known session create ignored', {
        sessionId,
      });
      return;
    }

    const closing = this.closingSessions.get(sessionId);
    if (closing) await closing;

    if (this.isTrackedOrSpawning(sessionId)) return;

    this.knownSessions.set(sessionId, {
      parentId,
      title,
      directory,
    });

    this.spawningSessions.add(sessionId);

    try {
      const serverRunning = await isServerRunning(this.serverUrl);
      if (!serverRunning) {
        log('[multiplexer-session-manager] server not running, skipping', {
          serverUrl: this.serverUrl,
        });
        return;
      }

      if (this.closingSessions.has(sessionId) || this.sessions.has(sessionId)) {
        return;
      }

      log(
        '[multiplexer-session-manager] child session created, spawning pane',
        {
          sessionId,
          parentId,
          title,
        },
      );

      const paneResult = await this.multiplexer
        .spawnPane(sessionId, title, this.serverUrl, directory)
        .catch((err) => {
          log('[multiplexer-session-manager] failed to spawn pane', {
            error: String(err),
          });
          return { success: false, paneId: undefined };
        });

      if (!paneResult.success || !paneResult.paneId) return;

      if (
        !this.knownSessions.has(sessionId) ||
        this.closingSessions.has(sessionId)
      ) {
        await this.multiplexer.closePane(paneResult.paneId).catch((err) =>
          log(
            '[multiplexer-session-manager] closing stale spawned pane failed',
            {
              sessionId,
              paneId: paneResult.paneId,
              error: String(err),
            },
          ),
        );
        return;
      }

      const now = Date.now();
      const pendingIdleSince = this.pendingIdleSessions.get(sessionId);
      this.sessions.set(sessionId, {
        sessionId,
        paneId: paneResult.paneId,
        parentId,
        title,
        directory,
        createdAt: now,
        lastSeenAt: now,
        hasSeenBusy: this.pendingBusySessions.has(sessionId),
        idleSince: pendingIdleSince,
      });
      this.pendingBusySessions.delete(sessionId);
      this.pendingIdleSessions.delete(sessionId);

      log('[multiplexer-session-manager] pane spawned', {
        sessionId,
        paneId: paneResult.paneId,
      });

      this.startPolling();
    } finally {
      this.spawningSessions.delete(sessionId);
    }
  }

  async onSessionStatus(event: SessionEvent): Promise<void> {
    if (!this.enabled) return;
    if (event.type !== 'session.status' && event.type !== 'session.idle') {
      return;
    }

    const sessionId = event.properties?.sessionID;
    if (!sessionId) return;

    if (
      event.type === 'session.idle' ||
      event.properties?.status?.type === 'idle'
    ) {
      await this.closeIfIdleConfirmed(sessionId, Date.now());
      return;
    }

    if (event.properties?.status?.type === 'busy') {
      this.markBusy(sessionId);
      await this.respawnIfKnown(sessionId);
    }
  }

  async onSessionDeleted(event: SessionEvent): Promise<void> {
    if (!this.enabled) return;
    if (event.type !== 'session.deleted') return;

    const sessionId = this.getSessionId(event);
    if (!sessionId) return;

    log('[multiplexer-session-manager] session deleted, closing pane', {
      sessionId,
    });

    await this.closeSession(sessionId, 'deleted');
  }

  private startPolling(): void {
    if (this.pollInterval) return;

    this.pollInterval = setInterval(
      () => this.pollSessions(),
      POLL_INTERVAL_BACKGROUND_MS,
    );
    log('[multiplexer-session-manager] polling started');
  }

  private stopPolling(): void {
    if (this.pollInterval) {
      clearInterval(this.pollInterval);
      this.pollInterval = undefined;
      log('[multiplexer-session-manager] polling stopped');
    }
  }

  private async pollSessions(): Promise<void> {
    if (this.sessions.size === 0) {
      this.stopPolling();
      return;
    }

    try {
      const statusResult = await this.client.session.status();
      const allStatuses = (statusResult.data ?? {}) as Record<
        string,
        { type: string }
      >;

      const now = Date.now();
      const sessionsToClose: Array<{ sessionId: string; reason: CloseReason }> =
        [];

      for (const [sessionId, tracked] of this.sessions.entries()) {
        const status = allStatuses[sessionId];
        const isIdle = status?.type === 'idle';

        if (status) {
          tracked.lastSeenAt = now;
          tracked.missingSince = undefined;
          if (status.type === 'busy') {
            this.markBusy(sessionId, now);
          }
        } else if (
          tracked.hasSeenBusy &&
          now - tracked.createdAt >= SESSION_IDLE_GRACE_MS &&
          !tracked.missingSince
        ) {
          tracked.missingSince = now;
        }

        const idleConfirmed = isIdle && this.markIdleAndCheck(tracked, now);
        const missingTooLong =
          tracked.hasSeenBusy &&
          !!tracked.missingSince &&
          now - tracked.missingSince >= SESSION_MISSING_GRACE_MS;
        const isTimedOut = now - tracked.createdAt > SESSION_TIMEOUT_MS;

        if (idleConfirmed || missingTooLong || isTimedOut) {
          sessionsToClose.push({
            sessionId,
            reason: idleConfirmed ? 'idle' : isTimedOut ? 'timeout' : 'missing',
          });
        }
      }

      for (const { sessionId, reason } of sessionsToClose) {
        await this.closeSession(sessionId, reason);
      }
    } catch (err) {
      log('[multiplexer-session-manager] poll error', { error: String(err) });
    }
  }

  private async closeSession(
    sessionId: string,
    reason: CloseReason,
  ): Promise<void> {
    if (reason === 'deleted') {
      this.knownSessions.delete(sessionId);
      this.pendingBusySessions.delete(sessionId);
      this.pendingIdleSessions.delete(sessionId);
    }

    const existingClose = this.closingSessions.get(sessionId);
    if (existingClose) return existingClose;

    const tracked = this.sessions.get(sessionId);
    if (!tracked || !this.multiplexer) return;

    this.sessions.delete(sessionId);

    log('[multiplexer-session-manager] closing session pane', {
      sessionId,
      paneId: tracked.paneId,
      reason,
    });

    const closePromise: Promise<void> = this.multiplexer
      .closePane(tracked.paneId)
      .then(() => undefined)
      .catch((err) =>
        log('[multiplexer-session-manager] failed to close session pane', {
          sessionId,
          paneId: tracked.paneId,
          reason,
          error: String(err),
        }),
      )
      .finally(() => {
        this.closingSessions.delete(sessionId);
        this.updatePolling();
      });

    this.closingSessions.set(sessionId, closePromise);
    await closePromise;
  }

  private async closeIfIdleConfirmed(
    sessionId: string,
    now: number,
  ): Promise<void> {
    const tracked = this.sessions.get(sessionId);
    if (!tracked) {
      if (
        this.spawningSessions.has(sessionId) &&
        this.knownSessions.has(sessionId)
      ) {
        this.pendingIdleSessions.set(sessionId, now);
      }
      return;
    }

    if (this.markIdleAndCheck(tracked, now)) {
      await this.closeSession(sessionId, 'idle');
    }
  }

  private markIdleAndCheck(tracked: TrackedSession, now: number): boolean {
    tracked.lastSeenAt = now;
    tracked.missingSince = undefined;

    if (!tracked.idleSince) {
      tracked.idleSince = now;
      log('[multiplexer-session-manager] idle observed, waiting to confirm', {
        sessionId: tracked.sessionId,
      });
      return false;
    }

    return (
      now - tracked.createdAt >= SESSION_IDLE_GRACE_MS &&
      now - tracked.idleSince >= SESSION_IDLE_DEBOUNCE_MS
    );
  }

  private markBusy(sessionId: string, now = Date.now()): void {
    const tracked = this.sessions.get(sessionId);
    if (tracked) {
      tracked.hasSeenBusy = true;
      tracked.idleSince = undefined;
      tracked.missingSince = undefined;
      tracked.lastSeenAt = now;
    } else if (
      this.spawningSessions.has(sessionId) &&
      this.knownSessions.has(sessionId)
    ) {
      this.pendingBusySessions.add(sessionId);
      this.pendingIdleSessions.delete(sessionId);
    }
  }

  private async respawnIfKnown(sessionId: string): Promise<void> {
    if (!this.enabled || !this.multiplexer) return;
    const closing = this.closingSessions.get(sessionId);
    if (closing) await closing;

    if (this.isTrackedOrSpawning(sessionId)) {
      return;
    }

    const known = this.knownSessions.get(sessionId);
    if (!known) return;

    this.spawningSessions.add(sessionId);

    try {
      const serverRunning = await isServerRunning(this.serverUrl);
      if (!serverRunning) {
        log(
          '[multiplexer-session-manager] server not running, skipping busy respawn',
          {
            serverUrl: this.serverUrl,
            sessionId,
          },
        );
        return;
      }

      if (this.sessions.has(sessionId) || this.closingSessions.has(sessionId)) {
        return;
      }

      log(
        '[multiplexer-session-manager] child session busy again, respawning pane',
        {
          sessionId,
          parentId: known.parentId,
          title: known.title,
        },
      );

      const paneResult = await this.multiplexer
        .spawnPane(sessionId, known.title, this.serverUrl, known.directory)
        .catch((err) => {
          log('[multiplexer-session-manager] failed to respawn pane', {
            error: String(err),
          });
          return { success: false, paneId: undefined };
        });

      if (!paneResult.success || !paneResult.paneId) return;

      if (
        !this.knownSessions.has(sessionId) ||
        this.closingSessions.has(sessionId)
      ) {
        await this.multiplexer.closePane(paneResult.paneId).catch((err) =>
          log(
            '[multiplexer-session-manager] closing stale respawned pane failed',
            {
              sessionId,
              paneId: paneResult.paneId,
              error: String(err),
            },
          ),
        );
        return;
      }

      const now = Date.now();
      this.sessions.set(sessionId, {
        sessionId,
        paneId: paneResult.paneId,
        parentId: known.parentId,
        title: known.title,
        directory: known.directory,
        createdAt: now,
        lastSeenAt: now,
        hasSeenBusy: true,
      });

      log('[multiplexer-session-manager] pane respawned on busy', {
        sessionId,
        paneId: paneResult.paneId,
      });

      this.startPolling();
    } finally {
      this.spawningSessions.delete(sessionId);
    }
  }

  private isTrackedOrSpawning(sessionId: string): boolean {
    return this.sessions.has(sessionId) || this.spawningSessions.has(sessionId);
  }

  private updatePolling(): void {
    if (this.sessions.size > 0 || this.closingSessions.size > 0) {
      this.startPolling();
    } else {
      this.stopPolling();
    }
  }

  private getSessionId(event: SessionEvent): string | undefined {
    return event.properties?.info?.id ?? event.properties?.sessionID;
  }

  async cleanup(): Promise<void> {
    this.stopPolling();

    if (this.closingSessions.size > 0) {
      await Promise.all(this.closingSessions.values());
    }

    if (this.sessions.size > 0 && this.multiplexer) {
      log('[multiplexer-session-manager] closing all panes', {
        count: this.sessions.size,
      });
      const multiplexer = this.multiplexer;
      const closePromises = Array.from(this.sessions.values()).map((s) =>
        multiplexer.closePane(s.paneId).catch((err) =>
          log('[multiplexer-session-manager] cleanup error for pane', {
            paneId: s.paneId,
            error: String(err),
          }),
        ),
      );
      await Promise.all(closePromises);
      this.sessions.clear();
    }

    this.knownSessions.clear();
    this.spawningSessions.clear();
    this.closingSessions.clear();
    this.pendingBusySessions.clear();
    this.pendingIdleSessions.clear();

    if (this.enabled) {
      MultiplexerSessionManager.hasActiveController = false;
      MultiplexerSessionManager.activeControllerKey = null;
    }

    log('[multiplexer-session-manager] cleanup complete');
  }
}

/**
 * @deprecated Use MultiplexerSessionManager instead
 */
export const TmuxSessionManager = MultiplexerSessionManager;
