import { logger } from '../logger.js';
import type { ContextConfig } from '../config.js';
import { estimateTokensFromChars } from './context/token-estimate.js';

export type ContextAction =
  | { type: 'warn'; ratio: number; tokens: number }
  | { type: 'prompt'; message: string }
  | { type: 'force_transition' };

const DEFAULT_CONTEXT_LIMIT = 128_000;
const EXHAUSTED_RATIO = 0.99;

const DEFAULT_THRESHOLDS: Array<{ ratio: number; action: 'warn' | 'prompt' | 'force_transition' }> = [
  { ratio: 0.7, action: 'warn' },
  { ratio: 0.85, action: 'prompt' },
  { ratio: 0.95, action: 'force_transition' },
];

function readPositiveInteger(value: unknown): number | null {
  if (typeof value === 'number' && Number.isFinite(value) && value > 0) {
    return Math.floor(value);
  }
  if (typeof value === 'string' && /^\d+$/.test(value.trim())) {
    const parsed = parseInt(value.trim(), 10);
    return parsed > 0 ? parsed : null;
  }
  return null;
}

function readNestedPositiveInteger(data: Record<string, unknown>, path: string[]): number | null {
  let current: unknown = data;
  for (const segment of path) {
    if (!current || typeof current !== 'object') return null;
    current = (current as Record<string, unknown>)[segment];
  }
  return readPositiveInteger(current);
}

function readContextLengthFromMetadata(metadata: unknown): number | null {
  if (!metadata || typeof metadata !== 'object') return null;
  for (const [key, value] of Object.entries(metadata as Record<string, unknown>)) {
    if (key.includes('context_length')) {
      const tokens = readPositiveInteger(value);
      if (tokens !== null) return tokens;
    }
  }
  return null;
}

export class ContextManager {
  private contextLimit: number;
  private lastPromptTokens = 0;
  private firedThresholds = new Set<number>();
  private hasUsage = false;
  private readonly sortedThresholds: Array<{ ratio: number; action: 'warn' | 'prompt' | 'force_transition' }>;

  constructor(config: ContextConfig) {
    this.contextLimit = config.limitTokens ?? DEFAULT_CONTEXT_LIMIT;
    const thresholds = config.thresholds ?? DEFAULT_THRESHOLDS;
    this.sortedThresholds = [...thresholds].sort((a, b) => a.ratio - b.ratio);
  }

  setContextLimit(tokens: number): void {
    this.contextLimit = tokens;
    logger.debug(`[context-manager] context limit set to ${tokens} tokens`);
  }

  update(usage: { prompt_tokens: number; completion_tokens: number }): ContextAction | null {
    this.lastPromptTokens = usage.prompt_tokens;
    this.hasUsage = true;
    return this.checkThresholds();
  }

  updateFromChars(totalChars: number): ContextAction | null {
    this.lastPromptTokens = estimateTokensFromChars(totalChars);
    this.hasUsage = true;
    return this.checkThresholds();
  }

  getRatio(): number {
    if (this.contextLimit <= 0) return 0;
    return this.lastPromptTokens / this.contextLimit;
  }

  isExhausted(): boolean {
    return this.getRatio() >= EXHAUSTED_RATIO;
  }

  hasUsageData(): boolean {
    return this.hasUsage;
  }

  getContextLimit(): number {
    return this.contextLimit;
  }

  getPromptTokens(): number {
    return this.lastPromptTokens;
  }

  /**
   * ツール結果を追加しても余裕のあるトークン数を返す。
   * - usage 取得済み: contextLimit - lastPromptTokens - completionReserve
   * - usage 未取得:   保守的に contextLimit * 0.4 を返す（system prompt/履歴の不確定を考慮）
   * completionReserve は次の LLM 応答（tool_call 含む）のための予約枠。
   */
  getAvailableTokens(completionReserve: number = 4_000): number {
    if (this.contextLimit <= 0) return 0;
    if (!this.hasUsage) {
      return Math.max(0, Math.floor(this.contextLimit * 0.4));
    }
    return Math.max(0, this.contextLimit - this.lastPromptTokens - completionReserve);
  }

  private checkThresholds(): ContextAction | null {
    const ratio = this.getRatio();
    for (const threshold of this.sortedThresholds) {
      if (ratio >= threshold.ratio && !this.firedThresholds.has(threshold.ratio)) {
        this.firedThresholds.add(threshold.ratio);
        logger.info(`[context-manager] threshold ${threshold.ratio} (${threshold.action}) fired at ratio=${ratio.toFixed(3)}`);
        return this.buildAction(threshold.action, ratio, this.lastPromptTokens);
      }
    }
    return null;
  }

  private buildAction(
    action: 'warn' | 'prompt' | 'force_transition',
    ratio: number,
    tokens: number,
  ): ContextAction {
    switch (action) {
      case 'warn':
        return { type: 'warn', ratio, tokens };
      case 'prompt':
        return {
          type: 'prompt',
          message: `コンテキストが逼迫しています（使用率: ${(ratio * 100).toFixed(0)}%）。作業をまとめて transition ツールで遷移してください。`,
        };
      case 'force_transition':
        return { type: 'force_transition' };
    }
  }
}

export async function fetchOllamaContextLimit(
  baseUrl: string,
  model: string,
  defaultLimit: number = 128_000,
): Promise<number> {
  const ollamaBase = baseUrl.replace(/\/v1\/?$/, '').replace(/\/+$/, '');
  try {
    const response = await fetch(`${ollamaBase}/api/show`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ name: model }),
      signal: AbortSignal.timeout(5_000),
    });
    if (!response.ok) {
      logger.debug(`[context-manager] /api/show returned ${response.status}, trying llama.cpp /props`);
    } else {
      const data = await response.json() as Record<string, unknown>;
      // Prefer the runtime num_ctx (what Ollama is actually serving) over
      // model_info.*.context_length (the model's theoretical max). Some Ollama
      // backends report a much higher theoretical context than they will accept
      // at runtime, which previously caused HTTP 400 from oversized prompts.
      const params = data['parameters'] as string | undefined;
      if (params) {
        const match = /num_ctx\s+(\d+)/.exec(params);
        if (match) {
          const numCtx = parseInt(match[1]!, 10);
          logger.debug(`[context-manager] auto-detected context limit from parameters: ${numCtx} tokens`);
          return numCtx;
        }
      }
      const modelInfoLimit = readContextLengthFromMetadata(data['model_info']);
      if (modelInfoLimit !== null) {
        logger.debug(`[context-manager] auto-detected context limit from model_info: ${modelInfoLimit} tokens`);
        return modelInfoLimit;
      }
    }
  } catch (err) {
    const message = err instanceof Error ? err.message : String(err);
    logger.debug(`[context-manager] failed to fetch /api/show context limit: ${message}, trying llama.cpp /props`);
  }

  try {
    const response = await fetch(`${ollamaBase}/props`, {
      method: 'GET',
      signal: AbortSignal.timeout(5_000),
    });
    if (!response.ok) {
      logger.debug(`[context-manager] /props returned ${response.status}, using default ${defaultLimit}`);
      return defaultLimit;
    }
    const data = await response.json() as Record<string, unknown>;
    const candidates = [
      readNestedPositiveInteger(data, ['default_generation_settings', 'n_ctx']),
      readNestedPositiveInteger(data, ['default_generation_settings', 'n_ctx_train']),
      readPositiveInteger(data['n_ctx']),
      readPositiveInteger(data['context_size']),
      readContextLengthFromMetadata(data['model_meta']),
      readContextLengthFromMetadata(data['model_info']),
    ];
    const contextLimit = candidates.find((value): value is number => value !== null);
    if (contextLimit !== undefined) {
      logger.debug(`[context-manager] auto-detected context limit from /props: ${contextLimit} tokens`);
      return contextLimit;
    }
    logger.debug(`[context-manager] could not find context limit in /props response, using default ${defaultLimit}`);
    return defaultLimit;
  } catch (err) {
    const message = err instanceof Error ? err.message : String(err);
    logger.debug(`[context-manager] failed to fetch /props context limit: ${message}, using default ${defaultLimit}`);
    return defaultLimit;
  }
}
