import { appendFileSync, mkdirSync } from 'fs';
import { join } from 'path';
import { Repository } from '../db/repository.js';
import { logger } from '../logger.js';
import { ActivityLogMetadata, formatActivityLogEntry, formatDuration, summarizeToolInput, truncateLogText } from './log-format.js';
import type { ContextAction } from '../engine/context-manager.js';

export class LocalProgressReporter {
  private logPath: string | null = null;

  constructor(
    private repo: Repository,
    private taskId: number,
    private workspacePath?: string,
    private metadata?: ActivityLogMetadata,
    private isSubTask: boolean = false,
  ) {
    if (workspacePath) {
      const logsDir = join(workspacePath, 'logs');
      mkdirSync(logsDir, { recursive: true });
      this.logPath = join(logsDir, 'activity.log');
    }
  }

  private appendLog(entry: string): void {
    if (!this.logPath) return;
    try {
      appendFileSync(this.logPath, `[${new Date().toISOString()}] ${formatActivityLogEntry(entry, this.metadata)}\n`);
    } catch (err) {
      logger.warn(`[local-reporter] failed to write log: ${err}`);
    }
  }

  async reportMovementStart(movementName: string): Promise<void> {
    this.appendLog(`[${movementName}] start`);
  }

  reportToolUse(toolName: string, input: Record<string, unknown>): void {
    this.appendLog(`${toolName}: ${summarizeToolInput(toolName, input)}`);
  }

  /**
   * Activity log line emitted immediately after a tool call completes.
   * Pairs with the preceding `reportToolUse` line so a human reading
   * activity.log can split "tool execution time" from "next LLM call time"
   * without consulting events.jsonl. Cache hits include a "(cache)" tag
   * so they don't get blamed for slow steps.
   */
  reportToolResult(toolName: string, info: { isError: boolean; durationMs: number; cacheHit: boolean }): void {
    const status = info.isError ? 'error' : 'ok';
    const cache = info.cacheHit ? ' (cache)' : '';
    this.appendLog(`${toolName}: ${status} ${formatDuration(info.durationMs)}${cache}`);
  }

  /**
   * Activity log line emitted after each LLM iteration completes. Together
   * with the `preflight` line (emitted just before the same LLM call) this
   * fully accounts for the wall-clock gap between two consecutive tool
   * calls: gap = `llm` line duration + the tool's own duration.
   */
  reportLLMCall(info: {
    durationMs: number;
    promptTokens?: number;
    completionTokens?: number;
    toolCalls: number;
    textChars: number;
  }): void {
    const tokenInfo = (typeof info.promptTokens === 'number' && typeof info.completionTokens === 'number')
      ? ` in=${info.promptTokens} out=${info.completionTokens}`
      : '';
    const shape = info.toolCalls > 0
      ? ` tools=${info.toolCalls}`
      : info.textChars > 0 ? ` text=${info.textChars}chars` : '';
    this.appendLog(`llm: ${formatDuration(info.durationMs)}${tokenInfo}${shape}`);
  }

  reportAssistantPreview(movementName: string, text: string): void {
    const trimmed = text.trim();
    if (!trimmed) return;
    this.appendLog(`[${movementName}] preview: ${truncateLogText(trimmed, 260)}`);
    if (this.isSubTask) return;
    const body = JSON.stringify({ type: 'thinking', text: trimmed, movement: movementName });
    this.repo.addLocalTaskComment(this.taskId, 'agent', body, 'progress').catch((err) => {
      logger.warn(`[local-reporter] failed to write thinking preview: ${err}`);
    });
  }

  reportContextAction(action: ContextAction): void {
    if (action.type === 'warn') {
      this.appendLog(`context-action: warn ratio=${(action.ratio * 100).toFixed(1)}% tokens=${action.tokens}`);
    } else if (action.type === 'prompt') {
      this.appendLog(`context-action: prompt "${action.message}"`);
    } else {
      this.appendLog(`context-action: force_transition`);
    }
  }

  reportPromptPreflight(line: string): void {
    // ok 行は冗長なため Progress には短縮版のみ記録する。
    // blocked 行はトラブル時の診断情報なのでフル行を残す。
    // stdout logger には引き続きフル行が出るため、デバッグ情報は失われない。
    if (line.startsWith('[llm-preflight:ok]')) {
      const estimated = /\bestimated=([\d,]+)/.exec(line)?.[1];
      const context = /\bcontext=([\d,]+)/.exec(line)?.[1];
      if (estimated && context) {
        this.appendLog(`preflight: ${estimated}/${context} tokens`);
      }
      return;
    }
    this.appendLog(line);
  }

  async reportMovementComplete(movementName: string, _output: string, next: string | null): Promise<void> {
    this.appendLog(`[${movementName}] complete -> ${next ?? 'done'}`);
  }

  async reportFinalResult(status: 'completed' | 'aborted' | 'error' | 'cancelled', output: string): Promise<number | null> {
    this.appendLog(`final: ${status}`);
    if (this.isSubTask) return null;
    const prefix = status === 'completed' ? '✅ 完了'
      : status === 'cancelled' ? '🛑 キャンセル'
      : status === 'aborted' ? '⚠️ 中断'
      : '❌ エラー';
    const body = output ? `${prefix}\n\n${output}` : `${prefix}\n\n(empty output)`;
    const comment = await this.repo.addLocalTaskComment(this.taskId, 'agent', body, 'result');
    return comment.id;
  }

  reportInterjectionAck(interjections: Array<{ id: number; body: string }>, movementName: string): void {
    this.appendLog(`interjection: ack ${interjections.length} message(s)`);
    // Persist the interjection TEXT (truncated) to activity.log too. The live injection
    // into the LLM message array is in-memory only and is lost on cancel/restart; without
    // this line a resumed agent reading activity.log sees only the ack count, not what the
    // user actually said.
    for (const { id, body } of interjections) {
      this.appendLog(`interjection: [#${id}] ${truncateLogText(body.trim(), 500)}`);
    }
    if (this.isSubTask) return;
    const commentIds = interjections.map((c) => c.id);
    const body = JSON.stringify({ type: 'interjection_ack', commentIds, movement: movementName });
    this.repo.addLocalTaskComment(this.taskId, 'agent', body, 'progress').catch((err) => {
      logger.warn(`[local-reporter] failed to write interjection ack: ${err}`);
    });
  }

  async reportAsk(question: string): Promise<void> {
    this.appendLog(`ask: ${question}`);
    if (this.isSubTask) return;
    await this.repo.addLocalTaskComment(this.taskId, 'agent', `❓ 確認事項\n\n${question}`, 'ask');
  }

  async reportError(error: string): Promise<number | null> {
    return this.reportFinalResult('error', error);
  }
}
