/**
 * Gate Plugin — gated session flow as a ProxyModelPlugin.
 *
 * When a session starts, it is "gated": only begin_session is visible.
 * After begin_session is called, the session ungates and all tools become accessible.
 * If a gated session tries to call a real tool, the gate auto-ungates via keyword extraction.
 *
 * This plugin replaces the hardcoded gate logic in router.ts.
 */
import type { JsonRpcRequest, JsonRpcResponse } from '../../types.js';
import type { ToolDefinition } from '../types.js';
import type { ProxyModelPlugin, PluginSessionContext } from '../plugin.js';
import { SessionGate } from '../../gate/session-gate.js';
import { TagMatcher, extractKeywordsFromToolCall, tokenizeDescription } from '../../gate/tag-matcher.js';
import type { TagMatchResult } from '../../gate/tag-matcher.js';
import { LlmPromptSelector } from '../../gate/llm-selector.js';
import type { ProviderRegistry } from '../../providers/registry.js';

export interface GatePluginConfig {
  gated?: boolean;
  providerRegistry?: ProviderRegistry | null;
  modelOverride?: string;
  byteBudget?: number;
}

const MAX_RESPONSE_CHARS = 24_000;

export function createGatePlugin(config: GatePluginConfig = {}): ProxyModelPlugin {
  const isGated = config.gated !== false;
  const tagMatcher = new TagMatcher(config.byteBudget);
  const llmSelector = config.providerRegistry
    ? new LlmPromptSelector(config.providerRegistry, config.modelOverride)
    : null;

  // Per-session state tracking (plugin-scoped, not global SessionGate)
  const sessionGate = new SessionGate();

  return {
    name: 'gate',
    description: 'Gated session flow: begin_session → prompt selection → ungate.',

    async onSessionCreate(ctx) {
      sessionGate.createSession(ctx.sessionId, isGated);

      // Register begin_session virtual tool
      ctx.registerTool(getBeginSessionTool(llmSelector), async (args, callCtx) => {
        return handleBeginSession(args, callCtx, sessionGate, tagMatcher, llmSelector);
      });

      // Register read_prompts virtual tool (available even when ungated)
      ctx.registerTool(getReadPromptsTool(), async (args, callCtx) => {
        return handleReadPrompts(args, callCtx, sessionGate, tagMatcher);
      });

      // Register propose_prompt virtual tool
      ctx.registerTool(getProposeTool(), async (args, callCtx) => {
        return handleProposePrompt(args, callCtx);
      });
    },

    async onSessionDestroy(ctx) {
      sessionGate.removeSession(ctx.sessionId);
    },

    async onInitialize(_request, ctx) {
      if (!isGated) return null;

      // Build gate instructions with prompt index
      const parts: string[] = [];
      const gateInstructions = await ctx.getSystemPrompt(
        'gate-instructions',
        'IMPORTANT: This project uses a gated session. You must call begin_session with keywords describing your task before using any other tools. This will provide you with relevant project context, policies, and guidelines.',
      );
      parts.push(`\n${gateInstructions}`);

      // Append tool inventory (names only)
      try {
        const tools = await ctx.discoverTools();
        if (tools.length > 0) {
          parts.push('\nAvailable MCP server tools (accessible after begin_session):');
          for (const t of tools) {
            parts.push(`  ${t.name}`);
          }
        }
      } catch {
        // Tool discovery is optional
      }

      // Append compact prompt index
      try {
        const promptIndex = await ctx.fetchPromptIndex();
        if (promptIndex.length > 0) {
          let displayIndex = promptIndex;
          if (displayIndex.length > 50) {
            displayIndex = displayIndex.filter((p) => p.priority >= 7);
          }
          displayIndex.sort((a, b) => b.priority - a.priority);

          parts.push('\nAvailable project prompts:');
          for (const p of displayIndex) {
            const summary = p.summary ? `: ${p.summary}` : '';
            parts.push(`- ${p.name} (priority ${p.priority})${summary}`);
          }
          parts.push(
            '\nChoose your begin_session keywords based on which of these prompts seem relevant to your task.',
          );
        }
      } catch {
        // Prompt index is optional
      }

      return { instructions: parts.join('\n') };
    },

    async onToolsList(tools, ctx) {
      // When gated, only show begin_session
      if (sessionGate.isGated(ctx.sessionId)) {
        return [getBeginSessionTool(llmSelector)];
      }

      // When ungated, show upstream tools + read_prompts + propose_prompt (no begin_session)
      return [...tools, getReadPromptsTool(), getProposeTool()];
    },

    async onToolCallBefore(toolName, args, request, ctx) {
      // If gated and trying to call a real tool, auto-ungate via keyword extraction
      if (sessionGate.isGated(ctx.sessionId)) {
        return handleGatedIntercept(request, ctx, toolName, args, sessionGate, tagMatcher);
      }
      return null;
    },
  };
}

// ── begin_session tool definition ──

function getBeginSessionTool(llmSelector: LlmPromptSelector | null): ToolDefinition {
  if (llmSelector) {
    return {
      name: 'begin_session',
      description: 'Start your session by describing what you want to accomplish. You will receive relevant project context, policies, and guidelines. This is required before using other tools.',
      inputSchema: {
        type: 'object',
        properties: {
          description: {
            type: 'string',
            description: "Describe what you're trying to do in a sentence or two (e.g. \"I want to pair a new Zigbee device with the hub\")",
          },
        },
        required: ['description'],
      },
    };
  }
  return {
    name: 'begin_session',
    description: 'Start your session by providing keywords that describe your current task. You will receive relevant project context, policies, and guidelines. This is required before using other tools.',
    inputSchema: {
      type: 'object',
      properties: {
        tags: {
          type: 'array',
          items: { type: 'string' },
          maxItems: 10,
          description: '3-7 keywords describing your current task (e.g. ["zigbee", "pairing", "mqtt"])',
        },
      },
      required: ['tags'],
    },
  };
}

function getReadPromptsTool(): ToolDefinition {
  return {
    name: 'read_prompts',
    description: 'Retrieve additional project prompts by keywords. Use this if you need more context about specific topics. Returns matched prompts and a list of other available prompts.',
    inputSchema: {
      type: 'object',
      properties: {
        tags: {
          type: 'array',
          items: { type: 'string' },
          maxItems: 10,
          description: 'Keywords to match against available prompts',
        },
      },
      required: ['tags'],
    },
  };
}

function getProposeTool(): ToolDefinition {
  return {
    name: 'propose_prompt',
    description: 'Propose a new prompt for this project. Creates a pending request that must be approved by a user before becoming active.',
    inputSchema: {
      type: 'object',
      properties: {
        name: { type: 'string', description: 'Prompt name (lowercase alphanumeric with hyphens, e.g. "debug-guide")' },
        content: { type: 'string', description: 'Prompt content text' },
      },
      required: ['name', 'content'],
    },
  };
}

// ── begin_session handler ──

async function handleBeginSession(
  args: Record<string, unknown>,
  ctx: PluginSessionContext,
  sessionGate: SessionGate,
  tagMatcher: TagMatcher,
  llmSelector: LlmPromptSelector | null,
): Promise<unknown> {
  const rawTags = args['tags'] as string[] | undefined;
  const description = args['description'] as string | undefined;

  let tags: string[];
  if (rawTags && Array.isArray(rawTags) && rawTags.length > 0) {
    tags = rawTags;
  } else if (description && description.trim().length > 0) {
    tags = tokenizeDescription(description);
  } else {
    throw new ToolError(-32602, 'Provide tags or description');
  }

  if (!sessionGate.isGated(ctx.sessionId)) {
    return {
      content: [{ type: 'text', text: 'Session already started. Use read_prompts to retrieve additional context.' }],
    };
  }

  const promptIndex = await ctx.fetchPromptIndex();

  // Primary: LLM selection. Fallback: deterministic tag matching.
  let matchResult: TagMatchResult;
  let reasoning = '';

  if (llmSelector) {
    try {
      const llmIndex = promptIndex.map((p) => ({
        name: p.name,
        priority: p.priority,
        summary: p.summary,
        chapters: p.chapters,
      }));
      const getSystemPromptFn = ctx.getSystemPrompt.bind(ctx);
      const llmResult = await llmSelector.selectPrompts(tags, llmIndex, getSystemPromptFn);
      reasoning = llmResult.reasoning;

      const selectedSet = new Set(llmResult.selectedNames);
      const selected = promptIndex.filter((p) => selectedSet.has(p.name));
      const remaining = promptIndex.filter((p) => !selectedSet.has(p.name));

      matchResult = tagMatcher.match(
        [...tags, ...llmResult.selectedNames],
        selected,
      );
      matchResult.remaining = [...matchResult.remaining, ...remaining];
    } catch {
      matchResult = tagMatcher.match(tags, promptIndex);
    }
  } else {
    matchResult = tagMatcher.match(tags, promptIndex);
  }

  // Ungate the session
  sessionGate.ungate(ctx.sessionId, tags, matchResult);
  ctx.queueNotification('notifications/tools/list_changed');

  // Audit: gate_decision for begin_session
  ctx.emitAuditEvent({
    timestamp: new Date().toISOString(),
    eventKind: 'gate_decision',
    source: 'client',
    verified: false,
    payload: {
      trigger: 'begin_session',
      clientIntent: { tags, description: description ?? null },
      matchedPrompts: matchResult.fullContent.map((p) => p.name),
      reasoning: reasoning || null,
    },
  });

  // Build response
  const responseParts: string[] = [];

  if (reasoning) {
    responseParts.push(`Selection reasoning: ${reasoning}\n`);
  }

  for (const p of matchResult.fullContent) {
    responseParts.push(`--- ${p.name} (priority: ${p.priority}) ---\n${p.content}\n`);
  }

  if (matchResult.indexOnly.length > 0) {
    responseParts.push('Additional matched prompts (use read_prompts to retrieve full content):');
    for (const p of matchResult.indexOnly) {
      responseParts.push(`  - ${p.name}: ${p.summary ?? 'No description'}`);
    }
    responseParts.push('');
  }

  if (matchResult.remaining.length > 0) {
    responseParts.push('Other available prompts:');
    for (const p of matchResult.remaining) {
      responseParts.push(`  - ${p.name}: ${p.summary ?? 'No description'}`);
    }
    responseParts.push('');
  }

  const encouragement = await ctx.getSystemPrompt(
    'gate-encouragement',
    'If any of the listed prompts seem relevant to your work, or if you encounter unfamiliar patterns, conventions, or constraints during implementation, use read_prompts({ tags: [...] }) to retrieve them. It is better to check and not need it than to proceed without important context.',
  );
  responseParts.push(encouragement);

  // Append tool inventory (names only)
  try {
    const tools = await ctx.discoverTools();
    if (tools.length > 0) {
      responseParts.push('\nAvailable MCP server tools:');
      for (const t of tools) {
        responseParts.push(`  ${t.name}`);
      }
    }
  } catch {
    // Tool discovery is optional
  }

  const retryInstruction = await ctx.getSystemPrompt(
    'gate-session-active',
    "The session is now active with full tool access. Proceed with the user's original request using the tools listed above.",
  );
  responseParts.push(`\n${retryInstruction}`);

  // Safety cap
  let text = responseParts.join('\n');
  if (text.length > MAX_RESPONSE_CHARS) {
    text = text.slice(0, MAX_RESPONSE_CHARS) + '\n\n[Response truncated. Use read_prompts to retrieve full content.]';
  }

  return { content: [{ type: 'text', text }] };
}

// ── read_prompts handler ──

async function handleReadPrompts(
  args: Record<string, unknown>,
  ctx: PluginSessionContext,
  sessionGate: SessionGate,
  tagMatcher: TagMatcher,
): Promise<unknown> {
  const tags = args['tags'] as string[] | undefined;

  if (!tags || !Array.isArray(tags) || tags.length === 0) {
    throw new ToolError(-32602, 'Missing or empty tags array');
  }

  const promptIndex = await ctx.fetchPromptIndex();

  // Filter out already-sent prompts
  const available = sessionGate.filterAlreadySent(ctx.sessionId, promptIndex);
  const matchResult = tagMatcher.match(tags, available);

  // Record retrieved prompts
  sessionGate.addRetrievedPrompts(
    ctx.sessionId,
    tags,
    matchResult.fullContent.map((p) => p.name),
  );

  // Audit: prompt_delivery
  ctx.emitAuditEvent({
    timestamp: new Date().toISOString(),
    eventKind: 'prompt_delivery',
    source: 'mcplocal',
    verified: true,
    payload: {
      queryTags: tags,
      deliveredPrompts: matchResult.fullContent.map((p) => p.name),
      indexOnlyPrompts: matchResult.indexOnly.map((p) => p.name),
    },
  });

  if (matchResult.fullContent.length === 0 && matchResult.indexOnly.length === 0) {
    return { content: [{ type: 'text', text: 'No new matching prompts found for the given keywords.' }] };
  }

  const responseParts: string[] = [];
  for (const p of matchResult.fullContent) {
    responseParts.push(`--- ${p.name} (priority: ${p.priority}) ---\n${p.content}\n`);
  }

  if (matchResult.indexOnly.length > 0) {
    responseParts.push('Additional matched prompts (too large to include, try more specific keywords):');
    for (const p of matchResult.indexOnly) {
      responseParts.push(`  - ${p.name}: ${p.summary ?? 'No description'}`);
    }
  }

  return { content: [{ type: 'text', text: responseParts.join('\n') }] };
}

// ── propose_prompt handler ──

async function handleProposePrompt(
  args: Record<string, unknown>,
  ctx: PluginSessionContext,
): Promise<unknown> {
  const name = args['name'] as string | undefined;
  const content = args['content'] as string | undefined;

  if (!name || !content) {
    throw new ToolError(-32602, 'Missing required arguments: name and content');
  }

  try {
    const body: Record<string, unknown> = { name, content };
    body['createdBySession'] = ctx.sessionId;
    await ctx.postToMcpd(
      `/api/v1/projects/${encodeURIComponent(ctx.projectName)}/promptrequests`,
      body,
    );
    return {
      content: [
        {
          type: 'text',
          text: `Prompt request "${name}" created successfully. It will be visible to you as a resource at mcpctl://prompts/${name}. A user must approve it before it becomes permanent.`,
        },
      ],
    };
  } catch (err) {
    throw new ToolError(-32603, `Failed to propose prompt: ${err instanceof Error ? err.message : String(err)}`);
  }
}

// ── gated intercept handler ──

async function handleGatedIntercept(
  request: JsonRpcRequest,
  ctx: PluginSessionContext,
  toolName: string,
  toolArgs: Record<string, unknown>,
  sessionGate: SessionGate,
  tagMatcher: TagMatcher,
): Promise<JsonRpcResponse> {
  const tags = extractKeywordsFromToolCall(toolName, toolArgs);

  try {
    const promptIndex = await ctx.fetchPromptIndex();
    const matchResult = tagMatcher.match(tags, promptIndex);

    // Ungate the session
    sessionGate.ungate(ctx.sessionId, tags, matchResult);
    ctx.queueNotification('notifications/tools/list_changed');

    // Audit: gate_decision for auto-intercept
    ctx.emitAuditEvent({
      timestamp: new Date().toISOString(),
      eventKind: 'gate_decision',
      source: 'mcplocal',
      verified: true,
      payload: {
        trigger: 'auto_intercept',
        toolName,
        extractedKeywords: tags,
        matchedPrompts: matchResult.fullContent.map((p) => p.name),
      },
    });

    // Build briefing from matched content
    const briefingParts: string[] = [];
    if (matchResult.fullContent.length > 0) {
      const preamble = await ctx.getSystemPrompt(
        'gate-intercept-preamble',
        'The following project context was automatically retrieved based on your tool call.',
      );
      briefingParts.push(`--- ${preamble} ---\n`);
      for (const p of matchResult.fullContent) {
        briefingParts.push(`--- ${p.name} (priority: ${p.priority}) ---\n${p.content}\n`);
      }
      briefingParts.push('--- End of project context ---\n');
    }

    if (matchResult.remaining.length > 0 || matchResult.indexOnly.length > 0) {
      briefingParts.push('Other prompts available (use read_prompts to retrieve):');
      for (const p of [...matchResult.indexOnly, ...matchResult.remaining]) {
        briefingParts.push(`  - ${p.name}: ${p.summary ?? 'No description'}`);
      }
      briefingParts.push('');
    }

    // Append tool inventory
    try {
      const tools = await ctx.discoverTools();
      if (tools.length > 0) {
        briefingParts.push('Available MCP server tools:');
        for (const t of tools) {
          briefingParts.push(`  ${t.name}`);
        }
        briefingParts.push('');
      }
    } catch {
      // Tool discovery is optional
    }

    // Route the actual tool call
    const response = await ctx.routeToUpstream(request);

    // Prepend briefing to the response
    if (briefingParts.length > 0 && response.result && !response.error) {
      const result = response.result as { content?: Array<{ type: string; text: string }> };
      const briefing = briefingParts.join('\n');
      if (result.content && Array.isArray(result.content)) {
        result.content.unshift({ type: 'text', text: briefing });
      } else {
        (response.result as Record<string, unknown>)['_briefing'] = briefing;
      }
    }

    return response;
  } catch {
    // If prompt retrieval fails, just ungate and route normally
    sessionGate.ungate(ctx.sessionId, tags, { fullContent: [], indexOnly: [], remaining: [] });
    ctx.queueNotification('notifications/tools/list_changed');
    return ctx.routeToUpstream(request);
  }
}

/** Error class for virtual tool errors that maps to JSON-RPC error codes. */
class ToolError extends Error {
  constructor(public readonly code: number, message: string) {
    super(message);
    this.name = 'ToolError';
  }
}
