import type { PluginInput } from '@opencode-ai/plugin';
import { tool } from '@opencode-ai/plugin/tool';
import { createInternalAgentTextPart, log } from '../../utils';

const HOOK_NAME = 'todo-continuation';
const COMMAND_NAME = 'auto-continue';

const CONTINUATION_PROMPT =
  '[Auto-continue: enabled - there are incomplete todos remaining. Continue with the next uncompleted item. Press Esc to cancel. If you need user input or review for the next item, ask instead of proceeding.]';

// Suppress window after user abort (Esc/Ctrl+C) to avoid immediately
// re-continuing something the user explicitly stopped
const SUPPRESS_AFTER_ABORT_MS = 5_000;

const QUESTION_PHRASES = [
  'would you like',
  'should i',
  'do you want',
  'please review',
  'let me know',
  'what do you think',
  'can you confirm',
  'would you prefer',
  'shall i',
  'any thoughts',
];

// Statuses that indicate a todo is terminal (won't be worked on further).
// Uses denylist approach: any status not listed here is considered incomplete.
const TERMINAL_TODO_STATUSES = ['completed', 'cancelled'];

interface ContinuationState {
  enabled: boolean;
  consecutiveContinuations: number;
  pendingTimer: ReturnType<typeof setTimeout> | null;
  suppressUntil: number;
  orchestratorSessionId: string | null;
  // True while our auto-injection prompt is in flight — prevents counter reset
  // on session.status→busy and blocks duplicate injections
  isAutoInjecting: boolean;
}

function isQuestion(text: string): boolean {
  const lowerText = text.toLowerCase().trim();
  // Match trailing '?' with optional whitespace after it
  if (/\?\s*$/.test(lowerText)) {
    return true;
  }
  return QUESTION_PHRASES.some((phrase) => lowerText.includes(phrase));
}

interface TodoItem {
  id: string;
  content: string;
  status: string;
  priority: string;
}

interface MessageInfo {
  role?: string;
  [key: string]: unknown;
}

interface MessagePart {
  type?: string;
  text?: string;
  [key: string]: unknown;
}

interface Message {
  info?: MessageInfo;
  parts?: MessagePart[];
}

function cancelPendingTimer(state: ContinuationState): void {
  if (state.pendingTimer) {
    clearTimeout(state.pendingTimer);
    state.pendingTimer = null;
  }
}

function resetState(state: ContinuationState): void {
  cancelPendingTimer(state);
  state.consecutiveContinuations = 0;
  state.suppressUntil = 0;
  state.isAutoInjecting = false;
}

export function createTodoContinuationHook(
  ctx: PluginInput,
  config?: {
    maxContinuations?: number;
    cooldownMs?: number;
    autoEnable?: boolean;
    autoEnableThreshold?: number;
  },
): {
  tool: Record<string, unknown>;
  handleEvent: (input: {
    event: { type: string; properties?: Record<string, unknown> };
  }) => Promise<void>;
  handleCommandExecuteBefore: (
    input: {
      command: string;
      sessionID: string;
      arguments: string;
    },
    output: { parts: Array<{ type: string; text?: string }> },
  ) => Promise<void>;
} {
  const maxContinuations = config?.maxContinuations ?? 5;
  const cooldownMs = config?.cooldownMs ?? 3000;
  const autoEnable = config?.autoEnable ?? false;
  const autoEnableThreshold = config?.autoEnableThreshold ?? 4;

  const state: ContinuationState = {
    enabled: false,
    consecutiveContinuations: 0,
    pendingTimer: null,
    suppressUntil: 0,
    orchestratorSessionId: null,
    isAutoInjecting: false,
  };

  const autoContinue = tool({
    description:
      'Toggle auto-continuation for incomplete todos. When enabled, the orchestrator will automatically continue working through its todo list when it stops with incomplete items.',
    args: { enabled: tool.schema.boolean() },
    execute: async (args) => {
      const enabled = args.enabled;
      state.enabled = enabled;
      state.consecutiveContinuations = 0;

      if (enabled) {
        state.suppressUntil = 0;
        log(`[${HOOK_NAME}] Auto-continue enabled`, { maxContinuations });
        return `Auto-continue enabled. Will auto-continue for up to ${maxContinuations} consecutive injections.`;
      }

      // Cancel any pending timer on disable
      cancelPendingTimer(state);
      log(`[${HOOK_NAME}] Auto-continue disabled`);
      return 'Auto-continue disabled.';
    },
  });

  async function handleEvent(input: {
    event: { type: string; properties?: Record<string, unknown> };
  }): Promise<void> {
    const { event } = input;
    const properties = event.properties ?? {};

    if (event.type === 'session.idle') {
      const sessionID = properties.sessionID as string;
      if (!sessionID) {
        return;
      }

      log(`[${HOOK_NAME}] Session idle`, { sessionID });

      // Track orchestrator session (assumes orchestrator is the first
      // session to go idle — correct for single-session main chat)
      if (!state.orchestratorSessionId) {
        state.orchestratorSessionId = sessionID;
        log(`[${HOOK_NAME}] Tracked orchestrator session`, {
          sessionID,
        });
      }

      // Gate: session is orchestrator (needed before auto-enable check)
      if (state.orchestratorSessionId !== sessionID) {
        log(`[${HOOK_NAME}] Skipped: not orchestrator session`, {
          sessionID,
        });
        return;
      }

      // Auto-enable check: if configured, not yet enabled, and enough
      // todos exist, automatically enable auto-continue.
      if (autoEnable && !state.enabled) {
        try {
          const todosResult = await ctx.client.session.todo({
            path: { id: sessionID },
          });
          const todos = todosResult.data as TodoItem[];
          const incompleteCount = todos.filter(
            (t) => !TERMINAL_TODO_STATUSES.includes(t.status),
          ).length;
          if (incompleteCount >= autoEnableThreshold) {
            state.enabled = true;
            state.consecutiveContinuations = 0;
            state.suppressUntil = 0;
            log(
              `[${HOOK_NAME}] Auto-enabled: ${incompleteCount} incomplete todos >= threshold ${autoEnableThreshold}`,
              { sessionID },
            );
          } else {
            log(
              `[${HOOK_NAME}] Auto-enable skipped: ${incompleteCount} incomplete todos < threshold ${autoEnableThreshold}`,
              { sessionID },
            );
          }
        } catch (error) {
          log(
            `[${HOOK_NAME}] Warning: failed to fetch todos for auto-enable check`,
            {
              sessionID,
              error: error instanceof Error ? error.message : String(error),
            },
          );
        }
      }

      // Safety gate 1: enabled
      if (!state.enabled) {
        log(`[${HOOK_NAME}] Skipped: auto-continue not enabled`, {
          sessionID,
        });
        return;
      }

      // Safety gate 2: incomplete todos exist
      let hasIncompleteTodos = false;
      let incompleteCount = 0;
      try {
        const todosResult = await ctx.client.session.todo({
          path: { id: sessionID },
        });
        const todos = todosResult.data as TodoItem[];
        incompleteCount = todos.filter(
          (t) => !TERMINAL_TODO_STATUSES.includes(t.status),
        ).length;
        hasIncompleteTodos = incompleteCount > 0;
        log(`[${HOOK_NAME}] Fetched todos`, {
          sessionID,
          hasIncompleteTodos,
          total: todos.length,
        });
      } catch (error) {
        log(`[${HOOK_NAME}] Warning: failed to fetch todos`, {
          sessionID,
          error: error instanceof Error ? error.message : String(error),
        });
        return;
      }

      if (!hasIncompleteTodos) {
        log(`[${HOOK_NAME}] Skipped: no incomplete todos`, { sessionID });
        return;
      }

      // Safety gate 3: last assistant message is not a question
      let lastAssistantIsQuestion = false;
      try {
        const messagesResult = await ctx.client.session.messages({
          path: { id: sessionID },
        });
        const messages = messagesResult.data as Message[];
        const lastAssistantMessage = messages
          .slice()
          .reverse()
          .find((m) => m.info?.role === 'assistant');
        if (lastAssistantMessage?.parts) {
          const lastText = lastAssistantMessage.parts
            .map((p) => p.text ?? '')
            .join(' ');
          lastAssistantIsQuestion = isQuestion(lastText);
        }
        log(`[${HOOK_NAME}] Fetched messages`, {
          sessionID,
          lastAssistantIsQuestion,
        });
      } catch (error) {
        log(`[${HOOK_NAME}] Warning: failed to fetch messages`, {
          sessionID,
          error: error instanceof Error ? error.message : String(error),
        });
        return;
      }

      if (lastAssistantIsQuestion) {
        log(`[${HOOK_NAME}] Skipped: last message is question`, {
          sessionID,
        });
        return;
      }

      // Safety gate 4: below max continuations
      if (state.consecutiveContinuations >= maxContinuations) {
        log(`[${HOOK_NAME}] Skipped: max continuations reached`, {
          sessionID,
          consecutive: state.consecutiveContinuations,
          max: maxContinuations,
        });
        return;
      }

      // Safety gate 5: not in suppress window
      const now = Date.now();
      if (now < state.suppressUntil) {
        log(`[${HOOK_NAME}] Skipped: in suppress window`, {
          sessionID,
          suppressUntil: state.suppressUntil,
        });
        return;
      }

      // Safety gate 6: no pending timer AND no injection in flight
      if (state.pendingTimer !== null || state.isAutoInjecting) {
        log(`[${HOOK_NAME}] Skipped: timer pending or injection in flight`, {
          sessionID,
        });
        return;
      }

      // Schedule continuation
      log(`[${HOOK_NAME}] Scheduling continuation`, {
        sessionID,
        delayMs: cooldownMs,
      });

      // Show countdown notification (noReply = agent doesn't respond)
      ctx.client.session
        .prompt({
          path: { id: sessionID },
          body: {
            noReply: true,
            parts: [
              {
                type: 'text',
                text: [
                  `⎔ Auto-continue: ${incompleteCount} incomplete todos remaining — resuming in ${cooldownMs / 1000}s — Esc×2 to cancel`,
                  '',
                  '[system status: continue without acknowledging this notification]',
                ].join('\n'),
              },
            ],
          },
        })
        .catch(() => {
          /* best-effort notification */
        });

      state.pendingTimer = setTimeout(async () => {
        state.pendingTimer = null;

        // Guard: may have been disabled during cooldown
        if (!state.enabled) {
          log(`[${HOOK_NAME}] Cancelled: disabled during cooldown`, {
            sessionID,
          });
          return;
        }

        state.isAutoInjecting = true;
        try {
          await ctx.client.session.prompt({
            path: { id: sessionID },
            body: {
              parts: [createInternalAgentTextPart(CONTINUATION_PROMPT)],
            },
          });
          state.consecutiveContinuations++;
          log(`[${HOOK_NAME}] Continuation injected`, {
            sessionID,
            consecutive: state.consecutiveContinuations,
          });
        } catch (error) {
          log(`[${HOOK_NAME}] Error: failed to inject continuation`, {
            sessionID,
            error: error instanceof Error ? error.message : String(error),
          });
        } finally {
          state.isAutoInjecting = false;
        }
      }, cooldownMs);
    } else if (event.type === 'session.status') {
      const status = properties.status as { type: string };
      const sessionID = properties.sessionID as string;
      if (status?.type === 'busy') {
        const isOrchestrator = sessionID === state.orchestratorSessionId;

        // Only cancel timer for orchestrator session — sub-agents going
        // busy must not silently kill the orchestrator's continuation.
        if (isOrchestrator) {
          cancelPendingTimer(state);
        }

        // Only reset consecutive counter for user-initiated activity,
        // not for our own auto-injection prompt. Scope to orchestrator only.
        if (
          !state.isAutoInjecting &&
          isOrchestrator &&
          state.consecutiveContinuations > 0
        ) {
          state.consecutiveContinuations = 0;
          log(`[${HOOK_NAME}] Reset consecutive count on user activity`, {
            sessionID,
          });
        }
      }
    } else if (event.type === 'session.error') {
      const error = properties.error as { name?: string };
      const sessionID = properties.sessionID as string;
      const errorName = error?.name;
      const isOrchestrator = sessionID === state.orchestratorSessionId;
      if (
        isOrchestrator &&
        (errorName === 'MessageAbortedError' || errorName === 'AbortError')
      ) {
        state.suppressUntil = Date.now() + SUPPRESS_AFTER_ABORT_MS;
        log(`[${HOOK_NAME}] Suppressed continuation after abort`, {
          sessionID,
          errorName,
        });
      }
      if (isOrchestrator) {
        cancelPendingTimer(state);
        log(`[${HOOK_NAME}] Cancelled pending timer on error`, {
          sessionID,
        });
      }
    } else if (event.type === 'session.deleted') {
      // OpenCode sends sessionID in two shapes:
      // properties.info.id (from session store) or properties.sessionID (from event)
      const deletedSessionId =
        (properties.info as { id?: string })?.id ??
        (properties.sessionID as string);

      // Only cancel timer if the orchestrator session itself was deleted.
      // Background sub-agent deletion must not kill the orchestrator's timer.
      if (state.orchestratorSessionId === deletedSessionId) {
        cancelPendingTimer(state);
        log(`[${HOOK_NAME}] Cancelled pending timer on orchestrator delete`, {
          sessionID: deletedSessionId,
        });

        resetState(state);
        state.orchestratorSessionId = null;
        log(`[${HOOK_NAME}] Reset orchestrator session on delete`, {
          sessionID: deletedSessionId,
        });
      }
    }
  }

  async function handleCommandExecuteBefore(
    input: {
      command: string;
      sessionID: string;
      arguments: string;
    },
    output: { parts: Array<{ type: string; text?: string }> },
  ): Promise<void> {
    if (input.command !== COMMAND_NAME) {
      return;
    }

    // Seed orchestrator session from slash command (more reliable than
    // first-idle heuristic — slash commands only fire in main chat)
    if (!state.orchestratorSessionId) {
      state.orchestratorSessionId = input.sessionID;
    }

    // Clear template text — hook handles everything directly
    output.parts.length = 0;

    // Accept explicit on/off argument, toggle only when no arg
    const arg = input.arguments.trim().toLowerCase();
    let newEnabled: boolean;
    if (arg === 'on') {
      newEnabled = true;
    } else if (arg === 'off') {
      newEnabled = false;
    } else {
      newEnabled = !state.enabled;
    }

    state.enabled = newEnabled;
    state.consecutiveContinuations = 0;

    if (!newEnabled) {
      // Cancel any pending timer on disable
      cancelPendingTimer(state);
      output.parts.push(
        createInternalAgentTextPart(
          '[Auto-continue: disabled by user command.]',
        ),
      );
      log(`[${HOOK_NAME}] Disabled via /${COMMAND_NAME} command`);
      return;
    }

    // Clear suppress window on explicit re-enable
    state.suppressUntil = 0;

    log(`[${HOOK_NAME}] Enabled via /${COMMAND_NAME} command`, {
      maxContinuations,
    });

    // Check for incomplete todos to decide on immediate continuation
    let hasIncompleteTodos = false;
    try {
      const todosResult = await ctx.client.session.todo({
        path: { id: input.sessionID },
      });
      const todos = todosResult.data as TodoItem[];
      hasIncompleteTodos = todos.some(
        (t) => !TERMINAL_TODO_STATUSES.includes(t.status),
      );
    } catch (error) {
      log(`[${HOOK_NAME}] Warning: failed to fetch todos in command hook`, {
        sessionID: input.sessionID,
        error: error instanceof Error ? error.message : String(error),
      });
    }

    if (hasIncompleteTodos) {
      output.parts.push(
        createInternalAgentTextPart(
          `${CONTINUATION_PROMPT} [Auto-continue enabled: up to ${maxContinuations} continuations.]`,
        ),
      );
    } else {
      output.parts.push(
        createInternalAgentTextPart(
          `[Auto-continue: enabled for up to ${maxContinuations} continuations. No incomplete todos right now.]`,
        ),
      );
    }
  }

  return {
    tool: { auto_continue: autoContinue },
    handleEvent,
    handleCommandExecuteBefore,
  };
}
