// Copyright (C) 2025 Keygraph, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License version 3
// as published by the Free Software Foundation.

/**
 * Workflow Logger
 *
 * Provides a unified, human-readable log file per workflow.
 * Optimized for `tail -f` viewing during concurrent workflow execution.
 */

import fs from 'node:fs/promises';
import { isFableModel, resolveModelId } from '../ai/models.js';
import { formatDuration, formatTimestamp } from '../utils/formatting.js';
import { LogStream } from './log-stream.js';
import { generateWorkflowLogPath, type SessionMetadata } from './utils.js';

export interface AgentLogDetails {
  attemptNumber?: number;
  duration_ms?: number;
  cost_usd?: number;
  success?: boolean;
  error?: string;
}

export interface AgentMetricsSummary {
  durationMs: number;
  costUsd: number | null;
}

export interface WorkflowSummary {
  status: 'completed' | 'failed' | 'cancelled' | 'partial';
  totalDurationMs: number;
  totalCostUsd: number;
  completedAgents: string[];
  agentMetrics: Record<string, AgentMetricsSummary>;
  error?: string;
}

/**
 * WorkflowLogger - Manages the unified workflow log file
 */
export class WorkflowLogger {
  private readonly sessionMetadata: SessionMetadata;
  private readonly logStream: LogStream;
  private workflowId: string | undefined;

  constructor(sessionMetadata: SessionMetadata) {
    this.sessionMetadata = sessionMetadata;
    const logPath = generateWorkflowLogPath(sessionMetadata);
    this.logStream = new LogStream(logPath);
  }

  /**
   * Initialize the log stream (creates file and writes header)
   */
  async initialize(workflowId?: string): Promise<void> {
    if (workflowId) {
      this.workflowId = workflowId;
    }

    if (this.logStream.isOpen) {
      return;
    }

    await this.logStream.open();

    // Write header only if file is new (empty)
    const stats = await fs.stat(this.logStream.path).catch(() => null);
    if (!stats || stats.size === 0) {
      await this.writeHeader();
    }
  }

  /**
   * Write header to log file
   */
  private async writeHeader(): Promise<void> {
    const lines = [
      `================================================================================`,
      `Shannon Pentest - Scan Log`,
      `================================================================================`,
      `Workflow ID: ${this.workflowId ?? this.sessionMetadata.id}`,
      `Target URL:  ${this.sessionMetadata.webUrl}`,
      `Started:     ${formatTimestamp()}`,
    ];

    // Surface Fable usage: its safety classifiers route cybersecurity tasks to
    // Opus 4.8, so those phases run on Opus 4.8 regardless of the tier setting.
    const fableTiers = (['small', 'medium', 'large'] as const)
      .map((tier) => ({ tier, model: resolveModelId(tier) }))
      .filter(({ model }) => isFableModel(model));
    if (fableTiers.length > 0) {
      const tierList = fableTiers.map(({ tier, model }) => `${tier} (${model})`).join(', ');
      lines.push(
        `Note:        ${tierList} set to a Fable model. Fable's safety classifiers`,
        `             route cybersecurity tasks to Opus 4.8, so those phases run on Opus 4.8.`,
      );
    }

    lines.push(`================================================================================`, ``);

    return this.logStream.write(lines.join('\n'));
  }

  /**
   * Write resume header to log file when workflow is resumed
   */
  async logResumeHeader(resumeInfo: {
    previousWorkflowId: string;
    newWorkflowId: string;
    checkpointHash: string;
    completedAgents: string[];
  }): Promise<void> {
    await this.ensureInitialized();

    const header = [
      ``,
      `================================================================================`,
      `RESUMED`,
      `================================================================================`,
      `Previous Workflow ID: ${resumeInfo.previousWorkflowId}`,
      `New Workflow ID:      ${resumeInfo.newWorkflowId}`,
      `Resumed At:           ${formatTimestamp()}`,
      `Checkpoint:           ${resumeInfo.checkpointHash}`,
      `Completed:            ${resumeInfo.completedAgents.length} agents (${resumeInfo.completedAgents.join(', ')})`,
      `================================================================================`,
      ``,
    ].join('\n');

    return this.logStream.write(header);
  }

  /**
   * Format timestamp for log line (UTC, human readable)
   */
  private formatLogTime(): string {
    const now = new Date();
    return now.toISOString().replace('T', ' ').slice(0, 19);
  }

  /**
   * Log a phase transition event
   */
  async logPhase(phase: string, event: 'start' | 'complete'): Promise<void> {
    await this.ensureInitialized();

    const action = event === 'start' ? 'Starting' : 'Completed';
    const line = `[${this.formatLogTime()}] [PHASE] ${action}: ${phase}\n`;

    // Add blank line before phase start for readability
    if (event === 'start') {
      await this.logStream.write('\n');
    }

    await this.logStream.write(line);
  }

  /**
   * Log an agent event
   */
  async logAgent(agentName: string, event: 'start' | 'end', details?: AgentLogDetails): Promise<void> {
    await this.ensureInitialized();

    let message: string;

    if (event === 'start') {
      const attempt = details?.attemptNumber ?? 1;
      message = `${agentName}: Starting (attempt ${attempt})`;
    } else {
      const parts: string[] = [`${agentName}:`];

      if (details?.success === false) {
        parts.push('Failed');
        if (details?.error) {
          parts.push(`- ${details.error}`);
        }
      } else {
        parts.push('Completed');
      }

      if (details?.duration_ms !== undefined) {
        parts.push(`(${formatDuration(details.duration_ms)}`);
        if (details?.cost_usd !== undefined) {
          parts.push(`$${details.cost_usd.toFixed(2)})`);
        } else {
          parts.push(')');
        }
      }

      message = parts.join(' ');
    }

    const line = `[${this.formatLogTime()}] [AGENT] ${message}\n`;
    await this.logStream.write(line);
  }

  /**
   * Log a general event
   */
  async logEvent(eventType: string, message: string): Promise<void> {
    await this.ensureInitialized();

    const line = `[${this.formatLogTime()}] [${eventType.toUpperCase()}] ${message}\n`;
    await this.logStream.write(line);
  }

  /**
   * Log an error
   */
  async logError(error: Error, context?: string): Promise<void> {
    await this.ensureInitialized();

    const contextStr = context ? ` (${context})` : '';
    const line = `[${this.formatLogTime()}] [ERROR] ${error.message}${contextStr}\n`;
    await this.logStream.write(line);
  }

  /**
   * Truncate string to max length with ellipsis
   */
  private truncate(str: string, maxLen: number): string {
    if (str.length <= maxLen) return str;
    return `${str.slice(0, maxLen - 3)}...`;
  }

  /**
   * Format tool parameters for human-readable display
   */
  private formatToolParams(toolName: string, params: unknown): string {
    if (!params || typeof params !== 'object') {
      return '';
    }

    const p = params as Record<string, unknown>;

    // Tool-specific formatting for common tools
    switch (toolName) {
      case 'Bash':
        if (p.command) {
          return this.truncate(String(p.command).replace(/\n/g, ' '), 100);
        }
        break;
      case 'Read':
        if (p.file_path) {
          return String(p.file_path);
        }
        break;
      case 'Write':
        if (p.file_path) {
          return String(p.file_path);
        }
        break;
      case 'Edit':
        if (p.file_path) {
          return String(p.file_path);
        }
        break;
      case 'Glob':
        if (p.pattern) {
          return String(p.pattern);
        }
        break;
      case 'Grep':
        if (p.pattern) {
          const path = p.path ? ` in ${p.path}` : '';
          return `"${this.truncate(String(p.pattern), 50)}"${path}`;
        }
        break;
      case 'WebFetch':
        if (p.url) {
          return String(p.url);
        }
        break;
    }

    // Default: show first string-valued param truncated
    for (const [key, val] of Object.entries(p)) {
      if (typeof val === 'string' && val.length > 0) {
        return `${key}=${this.truncate(val, 60)}`;
      }
    }

    return '';
  }

  /**
   * Log tool start event
   */
  async logToolStart(agentName: string, toolName: string, parameters: unknown): Promise<void> {
    await this.ensureInitialized();

    const params = this.formatToolParams(toolName, parameters);
    const paramStr = params ? `: ${params}` : '';
    const line = `[${this.formatLogTime()}] [${agentName}] [TOOL] ${toolName}${paramStr}\n`;
    await this.logStream.write(line);
  }

  /**
   * Log LLM response
   */
  async logLlmResponse(agentName: string, turn: number, content: string): Promise<void> {
    await this.ensureInitialized();

    // Show full content, replacing newlines with escaped version for single-line output
    const escaped = content.replace(/\n/g, '\\n');
    const line = `[${this.formatLogTime()}] [${agentName}] [LLM] Turn ${turn}: ${escaped}\n`;
    await this.logStream.write(line);
  }

  /**
   * Format a pipe-delimited error string into indented multi-line display.
   *
   * Input:  "phase context|ErrorType|message|Hint: ..."
   * Output: "Error:       phase context\n             ErrorType\n             ..."
   */
  private formatErrorBlock(errorString: string): string {
    const segments = errorString.split('|');
    const label = 'Error:       ';
    const indent = ' '.repeat(label.length);

    const lines = segments.map((segment, i) => (i === 0 ? `${label}${segment.trim()}` : `${indent}${segment.trim()}`));

    return `${lines.join('\n')}\n`;
  }

  /**
   * Log workflow completion with full summary
   */
  async logWorkflowComplete(summary: WorkflowSummary): Promise<void> {
    await this.ensureInitialized();

    const status = summary.status === 'completed' ? 'COMPLETED' : 'FAILED';

    const lines: string[] = [
      '',
      '================================================================================',
      `Scan ${status}`,
      '────────────────────────────────────────',
      `Workflow ID: ${this.workflowId ?? this.sessionMetadata.id}`,
      `Status:      ${summary.status}`,
      `Duration:    ${formatDuration(summary.totalDurationMs)}`,
      `Total Cost:  $${summary.totalCostUsd.toFixed(4)}`,
      `Agents:      ${summary.completedAgents.length} completed`,
    ];

    if (summary.error) {
      lines.push(this.formatErrorBlock(summary.error).trimEnd());
    }

    lines.push('');
    lines.push('Agent Breakdown:');

    for (const agentName of summary.completedAgents) {
      const metrics = summary.agentMetrics[agentName];
      if (metrics) {
        const duration = formatDuration(metrics.durationMs);
        const cost = metrics.costUsd !== null ? `$${metrics.costUsd.toFixed(4)}` : 'N/A';
        lines.push(`  - ${agentName} (${duration}, ${cost})`);
      } else {
        lines.push(`  - ${agentName}`);
      }
    }

    lines.push('================================================================================');

    // Single atomic write to prevent interleaved/duplicate output in log tailers
    await this.logStream.write(`${lines.join('\n')}\n`);
  }

  /**
   * Ensure initialized (helper for lazy initialization)
   */
  private async ensureInitialized(): Promise<void> {
    if (!this.logStream.isOpen) {
      await this.initialize();
    }
  }

  /**
   * Close the log stream
   */
  async close(): Promise<void> {
    return this.logStream.close();
  }
}
