import { ChildProcess, execSync } from 'child_process';
import fs from 'fs';
import path from 'path';

import { stopContainer } from './container-runtime.js';
import { resolveGroupIpcPath } from './group-folder.js';
import { logger } from './logger.js';
import type { AgentStatus } from './db.js';
import type { Attachment } from './types.js';

interface QueuedTask {
  id: string;
  groupJid: string;
  fn: () => Promise<void>;
}

const MAX_RETRIES = 5;
const BASE_RETRY_MS = 5000;
const SHUTDOWN_POLL_MS = 100;

function sleep(ms: number): Promise<void> {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

interface GroupState {
  active: boolean;
  idleWaiting: boolean;
  isTaskContainer: boolean;
  runningTaskId: string | null;
  pendingMessages: boolean;
  pendingTasks: QueuedTask[];
  process: ChildProcess | null;
  containerName: string | null;
  groupFolder: string | null;
  tenantId: string | null;
  retryCount: number;
  killSignaled: boolean;
}

export class GroupQueue {
  private groups = new Map<string, GroupState>();
  private activeCount = 0;
  private processMessagesFn: ((groupJid: string, onStartWorking?: () => void) => Promise<boolean>) | null =
    null;
  private onStatusChange: ((chatJid: string, status: AgentStatus) => void) | null = null;
  private shuttingDown = false;

  private getGroup(groupJid: string): GroupState {
    let state = this.groups.get(groupJid);
    if (!state) {
      state = {
        active: false,
        idleWaiting: false,
        isTaskContainer: false,
        runningTaskId: null,
        pendingMessages: false,
        pendingTasks: [],
        process: null,
        containerName: null,
        groupFolder: null,
        tenantId: null,
        retryCount: 0,
        killSignaled: false,
      };
      this.groups.set(groupJid, state);
    }
    return state;
  }

  setProcessMessagesFn(fn: (groupJid: string, onStartWorking?: () => void) => Promise<boolean>): void {
    this.processMessagesFn = fn;
  }

  setStatusChangeFn(fn: (chatJid: string, status: AgentStatus) => void): void {
    this.onStatusChange = fn;
  }

  private setStatus(chatJid: string, status: AgentStatus): void {
    try {
      this.onStatusChange?.(chatJid, status);
    } catch (err) {
      logger.warn({ chatJid, status, err }, 'Failed to update agent status');
    }
  }

  /**
   * Associate a group JID with a tenant for per-tenant limit tracking.
   */
  setGroupTenant(groupJid: string, tenantId: string): void {
    const state = this.getGroup(groupJid);
    state.tenantId = tenantId;
  }

  enqueueMessageCheck(groupJid: string): void {
    if (this.shuttingDown) return;

    const state = this.getGroup(groupJid);

    if (state.active) {
      state.pendingMessages = true;
      logger.debug({ groupJid }, 'Container active, message queued');
      return;
    }

    this.runForGroup(groupJid, 'messages').catch((err) =>
      logger.error({ groupJid, err }, 'Unhandled error in runForGroup'),
    );
  }

  enqueueTask(groupJid: string, taskId: string, fn: () => Promise<void>): void {
    if (this.shuttingDown) return;

    const state = this.getGroup(groupJid);

    // Prevent double-queuing
    if (state.runningTaskId === taskId) {
      logger.debug({ groupJid, taskId }, 'Task already running, skipping');
      return;
    }
    if (state.pendingTasks.some((t) => t.id === taskId)) {
      logger.debug({ groupJid, taskId }, 'Task already queued, skipping');
      return;
    }

    if (state.active) {
      state.pendingTasks.push({ id: taskId, groupJid, fn });
      if (state.idleWaiting) {
        this.closeStdin(groupJid);
      }
      logger.debug({ groupJid, taskId }, 'Container active, task queued');
      return;
    }

    // Run immediately
    this.runTask(groupJid, { id: taskId, groupJid, fn }).catch((err) =>
      logger.error({ groupJid, taskId, err }, 'Unhandled error in runTask'),
    );
  }

  registerProcess(
    groupJid: string,
    proc: ChildProcess,
    containerName: string,
    groupFolder?: string,
  ): void {
    const state = this.getGroup(groupJid);
    state.process = proc;
    state.containerName = containerName;
    if (groupFolder) state.groupFolder = groupFolder;
  }

  notifyIdle(groupJid: string): void {
    const state = this.getGroup(groupJid);
    state.idleWaiting = true;
    this.setStatus(groupJid, 'IDLE');
    if (state.pendingTasks.length > 0) {
      this.closeStdin(groupJid);
    }
  }

  canAcceptIpcMessage(groupJid: string): boolean {
    const state = this.getGroup(groupJid);
    return Boolean(
      state.active &&
      state.idleWaiting &&
      state.groupFolder &&
      !state.isTaskContainer,
    );
  }

  sendMessage(groupJid: string, text: string, attachments?: Attachment[]): boolean {
    const state = this.getGroup(groupJid);
    if (!this.canAcceptIpcMessage(groupJid))
      return false;
    state.idleWaiting = false;
    const groupFolder = state.groupFolder;
    if (!groupFolder) return false;

    const inputDir = path.join(resolveGroupIpcPath(groupFolder), 'input');
    try {
      fs.mkdirSync(inputDir, { recursive: true });
      const filename = `${Date.now()}-${Math.random().toString(36).slice(2, 6)}.json`;
      const filepath = path.join(inputDir, filename);
      const tempPath = `${filepath}.tmp`;
      fs.writeFileSync(tempPath, JSON.stringify({ type: 'message', text, attachments: attachments?.length ? attachments : undefined }));
      fs.renameSync(tempPath, filepath);
      this.setStatus(groupJid, 'WORKING');
      return true;
    } catch {
      return false;
    }
  }

  closeStdin(groupJid: string): void {
    const state = this.getGroup(groupJid);
    if (!state.active || !state.groupFolder) return;

    const inputDir = path.join(resolveGroupIpcPath(state.groupFolder), 'input');
    try {
      fs.mkdirSync(inputDir, { recursive: true });
      fs.writeFileSync(path.join(inputDir, '_close'), '');
    } catch {
      // ignore
    }
  }

  private async runForGroup(
    groupJid: string,
    reason: 'messages' | 'drain',
  ): Promise<void> {
    const state = this.getGroup(groupJid);
    state.active = true;
    state.idleWaiting = false;
    state.isTaskContainer = false;
    state.pendingMessages = false;
    state.killSignaled = false;
    this.activeCount++;
    // Don't set WORKING yet -- wait for processMessagesFn to confirm actual work exists.
    // This prevents the typing indicator from flashing on channels that end up with
    // nothing to process (e.g. parent channel whose cursor was advanced by thread auto-registration).
    let workingSignaled = false;
    const signalWorking = (): void => {
      if (!workingSignaled) {
        workingSignaled = true;
        this.setStatus(groupJid, 'WORKING');
      }
    };

    logger.debug(
      {
        groupJid,
        reason,
        activeCount: this.activeCount,
        tenantId: state.tenantId,
      },
      'Starting container for group',
    );

    try {
      if (this.processMessagesFn) {
        const success = await this.processMessagesFn(groupJid, signalWorking);
        if (success) {
          state.retryCount = 0;
        } else {
          this.scheduleRetry(groupJid, state);
        }
      }
    } catch (err) {
      logger.error({ groupJid, err }, 'Error processing messages for group');
      this.scheduleRetry(groupJid, state);
    } finally {
      state.active = false;
      state.process = null;
      state.containerName = null;
      state.groupFolder = null;
      this.activeCount--;
      this.setStatus(groupJid, 'IDLE');
      this.drainGroup(groupJid);
    }
  }

  private async runTask(groupJid: string, task: QueuedTask): Promise<void> {
    const state = this.getGroup(groupJid);
    state.active = true;
    state.idleWaiting = false;
    state.isTaskContainer = true;
    state.runningTaskId = task.id;
    this.activeCount++;
    this.setStatus(groupJid, 'WORKING');

    logger.debug(
      { groupJid, taskId: task.id, activeCount: this.activeCount },
      'Running queued task',
    );

    try {
      await task.fn();
    } catch (err) {
      logger.error({ groupJid, taskId: task.id, err }, 'Error running task');
    } finally {
      state.active = false;
      state.isTaskContainer = false;
      state.runningTaskId = null;
      state.process = null;
      state.containerName = null;
      state.groupFolder = null;
      this.activeCount--;
      this.setStatus(groupJid, 'IDLE');
      this.drainGroup(groupJid);
    }
  }

  private scheduleRetry(groupJid: string, state: GroupState): void {
    state.retryCount++;
    if (state.retryCount > MAX_RETRIES) {
      logger.error(
        { groupJid, retryCount: state.retryCount },
        'Max retries exceeded, dropping messages (will retry on next incoming message)',
      );
      state.retryCount = 0;
      return;
    }

    const delayMs = BASE_RETRY_MS * Math.pow(2, state.retryCount - 1);
    logger.info(
      { groupJid, retryCount: state.retryCount, delayMs },
      'Scheduling retry with backoff',
    );
    setTimeout(() => {
      if (!this.shuttingDown) {
        this.enqueueMessageCheck(groupJid);
      }
    }, delayMs);
  }

  private drainGroup(groupJid: string): void {
    if (this.shuttingDown) return;

    const state = this.getGroup(groupJid);

    // Tasks first
    if (state.pendingTasks.length > 0) {
      const task = state.pendingTasks.shift()!;
      this.runTask(groupJid, task).catch((err) =>
        logger.error(
          { groupJid, taskId: task.id, err },
          'Unhandled error in runTask (drain)',
        ),
      );
      return;
    }

    // Then pending messages
    if (state.pendingMessages) {
      this.runForGroup(groupJid, 'drain').catch((err) =>
        logger.error(
          { groupJid, err },
          'Unhandled error in runForGroup (drain)',
        ),
      );
      return;
    }

    // Nothing pending; check waiting groups
  }


  isKillSignaled(groupJid: string): boolean {
    return this.groups.get(groupJid)?.killSignaled ?? false;
  }

  /**
   * Kill the currently running container for a group.
   * Returns true if a container was killed, false if nothing was running.
   */
  async killContainer(groupJid: string): Promise<boolean> {
    const state = this.getGroup(groupJid);
    if (!state.active || !state.containerName) return false;

    const containerName = state.containerName;
    state.killSignaled = true;
    state.pendingMessages = false;  // prevent drainGroup from re-spawning

    // Kill the child process
    if (state.process && !state.process.killed) {
      state.process.kill('SIGTERM');
    }

    // Also stop the docker container (best-effort)
    try {
      execSync('docker kill ' + containerName, { timeout: 5000, stdio: 'ignore' });
    } catch {
      // Container may have already stopped
    }

    return true;
  }

  getActiveGroupJids(): string[] {
    const jids: string[] = [];
    for (const [jid, state] of this.groups) {
      if (state.active) {
        jids.push(jid);
      }
    }
    return jids;
  }

  /** Returns JIDs of groups that are actively working (not idle-waiting). */
  getWorkingGroupJids(): string[] {
    const jids: string[] = [];
    for (const [jid, state] of this.groups) {
      if (state.active && !state.idleWaiting) {
        jids.push(jid);
      }
    }
    return jids;
  }

  private async waitForIdle(timeoutMs: number): Promise<boolean> {
    let remaining = Math.max(0, timeoutMs);
    while (this.activeCount > 0 && remaining > 0) {
      const sleepMs = Math.min(SHUTDOWN_POLL_MS, remaining);
      await sleep(sleepMs);
      remaining -= sleepMs;
    }
    return this.activeCount === 0;
  }

  private requestIdleContainerExit(): string[] {
    const signaled: string[] = [];
    for (const [jid, state] of this.groups) {
      if (!state.active || !state.idleWaiting || !state.groupFolder) continue;
      signaled.push(jid);
      this.closeStdin(jid);
    }
    return signaled;
  }

  private stopActiveContainers(signal: 'SIGTERM' | 'SIGKILL'): string[] {
    const signaled: string[] = [];
    for (const [jid, state] of this.groups) {
      if (!state.active) continue;
      state.killSignaled = true;
      state.pendingMessages = false;
      state.pendingTasks = [];

      if (state.process) {
        try {
          state.process.kill(signal);
        } catch {
          // Ignore already-exited child processes.
        }
      }

      if (state.containerName) {
        try {
          if (signal === 'SIGTERM') {
            execSync(stopContainer(state.containerName), {
              timeout: 5000,
              stdio: 'ignore',
            });
          } else {
            execSync(`docker kill ${state.containerName}`, {
              timeout: 5000,
              stdio: 'ignore',
            });
          }
        } catch {
          // Container may have already stopped.
        }
      }

      signaled.push(jid);
    }
    return signaled;
  }

  async shutdown(gracePeriodMs: number): Promise<void> {
    this.shuttingDown = true;

    const idleWaitMs = Math.min(1000, Math.max(0, Math.floor(gracePeriodMs / 5)));
    const killWaitMs = Math.min(1000, Math.max(0, Math.floor(gracePeriodMs / 5)));
    const termWaitMs = Math.max(0, gracePeriodMs - idleWaitMs - killWaitMs);

    const idleSignals = this.requestIdleContainerExit();
    let drained = await this.waitForIdle(idleWaitMs);

    const termSignals = drained ? [] : this.stopActiveContainers('SIGTERM');
    if (!drained) {
      drained = await this.waitForIdle(termWaitMs);
    }

    const killSignals = drained ? [] : this.stopActiveContainers('SIGKILL');
    if (!drained) {
      drained = await this.waitForIdle(killWaitMs);
    }

    const remainingGroups = this.getActiveGroupJids();
    logger.info(
      {
        activeCount: this.activeCount,
        idleSignals,
        termSignals,
        killSignals,
        remainingGroups,
      },
      drained
        ? 'GroupQueue shutdown complete'
        : 'GroupQueue shutdown timed out with active groups remaining',
    );
  }
}
