import type { ModelClient, Message, ChatRequest, ChatResponse, ModelToolCall, TokenUsage } from '../../models/types.js';
import type { ModelRouter, ModelTier } from '../../models/router.js';
import type { Session } from '../../session/index.js';
import type { ToolRegistry } from '../../tools/registry.js';
import type { ToolExecutor } from '../../tools/executor.js';
import type { ToolResult } from '../../tools/types.js';
import type { ToolPolicyContext } from '../../tools/policy.js';
import { auditLogger } from '../../audit/index.js';
import type { Attachment } from '../../channels/types.js';
import type { OutboundAttachmentCollector } from './attachments.js';
import { buildUserMessage, getMessageText } from '../../models/media.js';

export interface ToolUseEvent {
  type: 'start' | 'end';
  tool: string;
  args?: unknown;
  result?: ToolResult;
}

export interface NativeAgentConfig {
  modelClient: ModelClient | ModelRouter;
  systemPrompt: string;
  session?: Session;
  toolRegistry?: ToolRegistry;
  toolExecutor?: ToolExecutor;
  maxIterations?: number;
  onToolUse?: (event: ToolUseEvent) => void;
  /** Policy context for tool filtering (agent tier, provider). */
  toolPolicyContext?: ToolPolicyContext;
  /** Collector for outbound attachments queued by tools (e.g. media.send). */
  attachmentCollector?: OutboundAttachmentCollector;
}

// Internal message type for the tool loop — supports both text and structured content blocks.
// This is broader than Message to accommodate Anthropic's tool_use/tool_result block format.
interface LoopMessage {
  role: 'user' | 'assistant';
  content: string | unknown[];
}

export class NativeAgent {
  private modelClient: ModelClient | ModelRouter;
  private systemPrompt: string;
  private session?: Session;
  private inMemoryHistory: Message[] = [];
  private currentTier: ModelTier = 'default';
  private toolRegistry?: ToolRegistry;
  private toolExecutor?: ToolExecutor;
  private maxIterations: number;
  private onToolUse?: (event: ToolUseEvent) => void;
  private _totalUsage: TokenUsage = { inputTokens: 0, outputTokens: 0 };
  private _callCount: number = 0;
  private _toolPolicyContext?: ToolPolicyContext;
  private _attachmentCollector?: OutboundAttachmentCollector;
  private _thinking: boolean = false;
  private _lastToolFingerprint?: string;
  private _cancelRequested = false;
  private _runInProgress = false;

  constructor(config: NativeAgentConfig) {
    this.modelClient = config.modelClient;
    this.systemPrompt = config.systemPrompt;
    this.session = config.session;
    this.toolRegistry = config.toolRegistry;
    this.toolExecutor = config.toolExecutor;
    this.maxIterations = config.maxIterations ?? 10;
    this.onToolUse = config.onToolUse;
    this._toolPolicyContext = config.toolPolicyContext;
    this._attachmentCollector = config.attachmentCollector;
  }

  private get history(): Message[] {
    return this.session?.getHistory() ?? [...this.inMemoryHistory];
  }

  async process(userMessage: string, attachments?: Attachment[]): Promise<string> {
    this._cancelRequested = false;
    this._runInProgress = true;

    // Detect and strip !!think prefix for per-message thinking mode
    try {
      if (userMessage.startsWith('!!think ') || userMessage === '!!think') {
        this._thinking = true;
        userMessage = userMessage.replace(/^!!think\s*/, '').trim() || 'Think about this.';
      } else {
        this._thinking = false;
      }

      const userMsg = buildUserMessage(userMessage, attachments);

      if (this.session) {
        this.session.addMessage(userMsg);
      } else {
        this.inMemoryHistory.push(userMsg);
      }

      // If no tools configured, use the simple single-turn path
      if (!this.toolRegistry || !this.toolExecutor) {
        return await this.singleTurn();
      }

      return await this.toolLoop();
    } catch (error) {
      if (this.isAbortError(error)) {
        const cancelledMsg = 'Operation cancelled by user.';
        this.addToHistory({ role: 'assistant', content: cancelledMsg });
        return cancelledMsg;
      }
      throw error;
    } finally {
      this._runInProgress = false;
      this._cancelRequested = false;
    }
  }

  private async singleTurn(): Promise<string> {
    this.throwIfCancelled();

    const request: ChatRequest = {
      messages: this.history,
      system: this.systemPrompt,
      ...(this._thinking ? { thinking: true } : {}),
    };

    const response = await this.chatWithRouter(request);
    this.throwIfCancelled();

    this._totalUsage.inputTokens += response.usage.inputTokens;
    this._totalUsage.outputTokens += response.usage.outputTokens;
    this._callCount++;

    // Prepend thinking content if present
    let finalContent = response.content;
    if (response.thinkingContent) {
      finalContent = `<thinking>\n${response.thinkingContent}\n</thinking>\n\n${response.content}`;
    }

    const assistantMsg: Message = { role: 'assistant', content: response.content };
    this.addToHistory(assistantMsg);

    return finalContent;
  }

  private async toolLoop(): Promise<string> {
    const tools = this.toolRegistry!.filteredToAnthropicFormat(this._toolPolicyContext);

    // Track whether untrusted content (web/fetched/tool output) has been introduced
    // during this run. Used to harden against prompt injection.
    let untrustedContentSeen = false;

    // Detect tool inventory changes to combat conversational inertia in long sessions.
    // When tools change (e.g. new tools added between restarts), the model's prior messages
    // saying "I can't do that" can override tool definitions. Injecting a system note fixes this.
    const currentFingerprint = tools.map(t => t.name).sort().join(',');
    const hasHistory = this.history.length > 1; // more than just the current user message
    let effectiveSystem = this.systemPrompt;

    if (hasHistory && this._lastToolFingerprint !== currentFingerprint) {
      const toolNames = tools.map(t => t.name).join(', ');
      effectiveSystem += `\n\n[Tool inventory updated — available tools: ${toolNames}. Use these tools directly; do not attempt workarounds for functionality that a tool already provides.]`;
    }
    this._lastToolFingerprint = currentFingerprint;

    // Build the loop messages from existing history.
    // These are the messages sent to the model, including any structured tool blocks.
    const loopMessages: LoopMessage[] = this.history.map(m => ({
      role: m.role,
      content: m.content,
    }));

    // Track consecutive identical tool call fingerprints to detect loops.
    // Local LLMs are especially prone to repeatedly requesting the same tool call.
    let lastFingerprint: string | undefined;
    let consecutiveRepeats = 0;
    const maxConsecutiveRepeats = 3;
    let lastToolResults: string[] = [];

    // Track consecutive calls to the same tool (even with different args).
    // Local models often call the same tool with slight query variations.
    let lastToolName: string | undefined;
    let sameToolStreak = 0;
    const maxSameToolStreak = 4; // nudge after 4 calls to the same tool
    let nudged = false;

    for (let iteration = 0; iteration < this.maxIterations; iteration++) {
      try {
        this.throwIfCancelled();

        // Build request — cast loopMessages to Message[] because the underlying
        // model client will pass them through to the API which accepts structured content.
        const request = {
          messages: loopMessages as unknown as Message[],
          system: effectiveSystem,
          tools,
          ...(this._thinking ? { thinking: true } : {}),
        };

        const response = await this.chatWithRouter(request);
        this.throwIfCancelled();

        this._totalUsage.inputTokens += response.usage.inputTokens;
        this._totalUsage.outputTokens += response.usage.outputTokens;
        this._callCount++;

        // If the model didn't request tool use, we're done.
        // Check both 'tool_use' (Anthropic) and 'tool_calls' (OpenAI-compatible) stop reasons,
        // but always require actual toolCalls to be present.
        const wantsToolUse = (response.stopReason === 'tool_use' || response.stopReason === 'tool_calls')
          && response.toolCalls && response.toolCalls.length > 0;
        if (!wantsToolUse) {
          let finalContent = response.content;
          if (response.thinkingContent) {
            finalContent = `<thinking>\n${response.thinkingContent}\n</thinking>\n\n${response.content}`;
          }
          const assistantMsg: Message = { role: 'assistant', content: response.content };
          this.addToHistory(assistantMsg);
          return finalContent;
        }

        // Safe to assert non-null — wantsToolUse guarantees toolCalls exists and is non-empty
        const toolCalls = response.toolCalls!;

        // Check for repeated tool calls — build a fingerprint from tool names + args
        const fingerprint = toolCalls
          .map(tc => `${tc.name}:${JSON.stringify(tc.args)}`)
          .sort()
          .join('|');

        if (fingerprint === lastFingerprint) {
          consecutiveRepeats++;
        } else {
          consecutiveRepeats = 1;
          lastFingerprint = fingerprint;
        }

        // Track consecutive calls to the same tool (by name, ignoring args)
        const toolNames = toolCalls.map(tc => tc.name).sort().join(',');
        if (toolNames === lastToolName) {
          sameToolStreak++;
        } else {
          sameToolStreak = 1;
          lastToolName = toolNames;
          nudged = false;
        }

        // Build the assistant message with tool_use content blocks
        const assistantContent: unknown[] = [];
        if (response.content) {
          assistantContent.push({ type: 'text', text: response.content });
        }
        for (const tc of toolCalls) {
          assistantContent.push({
            type: 'tool_use',
            id: tc.id,
            name: tc.name,
            input: tc.args,
          });
        }
        loopMessages.push({ role: 'assistant', content: assistantContent });

        // Execute each tool call and collect results
        const toolResultBlocks: unknown[] = [];
        lastToolResults = [];
        for (const tc of toolCalls) {
          this.throwIfCancelled();

          const internalName = this.toolRegistry!.getByApiName(tc.name)?.name ?? tc.name;
          this.onToolUse?.({ type: 'start', tool: internalName, args: tc.args });

          let elevationUntilMs: number | undefined;
          let elevationReason: string | undefined;
          let elevationId: string | undefined;

          if (this.session) {
            const untilRaw = this.session.getConfig('elevation.until_ms');
            const idRaw = this.session.getConfig('elevation.id');
            const reasonRaw = this.session.getConfig('elevation.reason');
            if (untilRaw && idRaw) {
              const untilMs = Number.parseInt(untilRaw, 10);
              if (Number.isFinite(untilMs)) {
                const now = Date.now();
                if (untilMs > now) {
                  elevationUntilMs = untilMs;
                  elevationId = idRaw;
                  elevationReason = reasonRaw ?? undefined;
                } else {
                  // Auto-expire elevation.
                  this.session.deleteConfig('elevation.until_ms');
                  this.session.deleteConfig('elevation.reason');
                  this.session.deleteConfig('elevation.id');
                  auditLogger?.securityElevationExpired({
                    session_id: this.session.id,
                    channel: this._toolPolicyContext?.channel ?? 'unknown',
                    sender: this._toolPolicyContext?.sender ?? 'unknown',
                    elevation_id: idRaw,
                    until_ms: untilMs,
                    reason: reasonRaw ?? undefined,
                  });
                }
              }
            }
          }

          const perCallContext: ToolPolicyContext | undefined = this._toolPolicyContext
            ? {
              ...this._toolPolicyContext,
              untrustedContent: untrustedContentSeen,
              elevatedHostUntilMs: elevationUntilMs,
              elevatedHostReason: elevationReason,
              elevatedHostId: elevationId,
            }
            : undefined;

          const result = await this.toolExecutor!.execute(internalName, tc.args, perCallContext);

          this.onToolUse?.({ type: 'end', tool: internalName, result });

          const provenance = (internalName === 'web.fetch' || internalName === 'web.search' || internalName === 'browser.content')
            ? 'fetched_content'
            : 'tool_output';

          if (provenance === 'fetched_content') {
            untrustedContentSeen = true;
          }

          const rawContent = result.success ? result.output : (result.error ?? 'Unknown error');
          const resultContent = `[provenance=${provenance} tool=${internalName} untrusted=${provenance === 'fetched_content' ? 'true' : 'false'}]\n${rawContent}\n[/provenance]`;
          toolResultBlocks.push({
            type: 'tool_result',
            tool_use_id: tc.id,
            content: resultContent,
            is_error: !result.success,
          });
          if (result.success && result.output) {
            lastToolResults.push(result.output);
          }
        }

        // If the same tool has been called too many times, append a nudge
        // telling the model to use what it has. This combats local models
        // that endlessly retry searches with slight query variations.
        if (sameToolStreak >= maxSameToolStreak && !nudged) {
          nudged = true;
          toolResultBlocks.push({
            type: 'tool_result',
            tool_use_id: '__system',
            content: `You have called this tool ${sameToolStreak} times in a row. You have enough information — do NOT call it again. Summarize what you have found and respond to the user now.`,
            is_error: false,
          });
        }

        // Add tool results as a user message
        loopMessages.push({ role: 'user', content: toolResultBlocks });

        // Break out if the model is stuck in a repeated tool call loop
        if (consecutiveRepeats >= maxConsecutiveRepeats) {
          const toolOutput = lastToolResults.length > 0
            ? lastToolResults.join('\n\n')
            : 'No results available.';
          const breakMsg = `Tool loop detected (same tool called ${consecutiveRepeats} times). Returning last results:\n\n${toolOutput}`;
          const assistantMsg: Message = { role: 'assistant', content: breakMsg };
          this.addToHistory(assistantMsg);
          return breakMsg;
        }
      } catch (error) {
        if (this.isAbortError(error)) {
          const cancelledMsg = 'Operation cancelled by user.';
          const assistantMsg: Message = { role: 'assistant', content: cancelledMsg };
          this.addToHistory(assistantMsg);
          return cancelledMsg;
        }

        const errorMsg = `Error in tool loop (iteration ${iteration + 1}): ${error instanceof Error ? error.message : String(error)}`;
        const assistantMsg: Message = { role: 'assistant', content: errorMsg };
        this.addToHistory(assistantMsg);
        return errorMsg;
      }
    }

    // Max iterations reached
    const warningMsg = `Stopped after reaching max iterations (${this.maxIterations}). The task may be incomplete.`;
    const assistantMsg: Message = { role: 'assistant', content: warningMsg };
    this.addToHistory(assistantMsg);
    return warningMsg;
  }

  private async chatWithRouter(request: ChatRequest): Promise<ChatResponse> {
    if ('getClient' in this.modelClient) {
      return (this.modelClient as ModelRouter).chat(request, this.currentTier);
    }
    return this.modelClient.chat(request);
  }

  private addToHistory(msg: Message): void {
    if (this.session) {
      this.session.addMessage(msg);
    } else {
      this.inMemoryHistory.push(msg);
    }
  }

  reset(): void {
    if (this.session) {
      this.session.clear();
    } else {
      this.inMemoryHistory = [];
    }
    this._lastToolFingerprint = undefined;
    this.resetUsage();
  }

  getUsage(): { inputTokens: number; outputTokens: number; calls: number } {
    return { ...this._totalUsage, calls: this._callCount };
  }

  resetUsage(): void {
    this._totalUsage = { inputTokens: 0, outputTokens: 0 };
    this._callCount = 0;
  }

  getHistory(): Message[] {
    return [...this.history];
  }

  setModelTier(tier: ModelTier): void {
    this.currentTier = tier;
  }

  getModelTier(): ModelTier {
    return this.currentTier;
  }

  setSystemPrompt(prompt: string): void {
    this.systemPrompt = prompt;
  }

  setOnToolUse(callback: ((event: ToolUseEvent) => void) | undefined): void {
    this.onToolUse = callback;
  }

  setToolPolicyContext(context: ToolPolicyContext | undefined): void {
    this._toolPolicyContext = context;
  }

  getToolPolicyContext(): ToolPolicyContext | undefined {
    return this._toolPolicyContext;
  }

  setAttachmentCollector(collector: OutboundAttachmentCollector | undefined): void {
    this._attachmentCollector = collector;
  }

  getAttachmentCollector(): OutboundAttachmentCollector | undefined {
    return this._attachmentCollector;
  }

  cancel(): void {
    if (this._runInProgress) {
      this._cancelRequested = true;
    }
  }

  isCancellable(): boolean {
    return this._runInProgress;
  }

  private throwIfCancelled(): void {
    if (!this._cancelRequested) {
      return;
    }

    const error = new Error('Operation cancelled by user.');
    error.name = 'AbortError';
    throw error;
  }

  private isAbortError(error: unknown): boolean {
    return error instanceof Error && error.name === 'AbortError';
  }
}
