import { getTaskDisplayId, taskMatchesRef } from '@shared/utils/taskIdentity';

import { TeamTaskReader } from '../../TeamTaskReader';
import { BoardTaskActivityRecordSource } from '../activity/BoardTaskActivityRecordSource';
import { TeamTranscriptSourceLocator } from '../discovery/TeamTranscriptSourceLocator';
import { BoardTaskExactLogStrictParser } from '../exact/BoardTaskExactLogStrictParser';
import { BoardTaskLogStreamService } from '../stream/BoardTaskLogStreamService';
import { HistoricalBoardMcpRawProbe } from '../stream/HistoricalBoardMcpRawProbe';
import { TaskLogTranscriptCandidateSelector } from '../stream/TaskLogTranscriptCandidateSelector';

import type { BoardTaskActivityRecord } from '../activity/BoardTaskActivityRecord';
import type { ParsedMessage } from '@main/types';
import type { TaskWorkInterval, TeamTask } from '@shared/types';

const BOARD_MCP_TOOL_PREFIXES = ['mcp__agent-teams__', 'mcp__agent_teams__'] as const;
const MAX_EXAMPLES = 10;

export interface BoardTaskLogDiagnosticExample {
  timestamp: string;
  filePath: string;
  messageUuid: string;
  toolUseId?: string;
  toolName: string;
  isSidechain: boolean;
  agentId?: string;
}

export interface BoardTaskLogDiagnosticsReport {
  teamName: string;
  requestedTaskRef: string;
  task: {
    taskId: string;
    displayId: string;
    subject: string;
    status: TeamTask['status'];
    owner?: string;
    workIntervals: TaskWorkInterval[];
  };
  transcript: {
    fileCount: number;
    files: string[];
    parsedFileCount: number;
    candidateSelection?: {
      mode: 'activity_records' | 'historical_raw_probe' | 'none';
      candidateFileCount: number;
      rawProbeScannedFileCount?: number;
      rawProbeHitCount?: number;
    };
  };
  explicitRecords: {
    total: number;
    execution: number;
    lifecycle: number;
    boardAction: number;
    participants: string[];
    toolNames: string[];
  };
  intervalToolResults: {
    total: number;
    boardMcp: number;
    worker: {
      total: number;
      explicitLinked: number;
      missingExplicit: number;
      examples: BoardTaskLogDiagnosticExample[];
    };
  };
  stream: {
    participants: string[];
    defaultFilter: string;
    segmentCount: number;
    visibleToolNames: string[];
    emptyPayloadExamples: BoardTaskLogDiagnosticExample[];
  };
  diagnosis: string[];
}

function normalizeRequestedTaskRef(taskRef: string): string {
  return taskRef.trim().replace(/^#/, '');
}

function isBoardMcpToolName(toolName: string | undefined): boolean {
  if (!toolName) return false;
  const normalized = toolName.trim().toLowerCase();
  return BOARD_MCP_TOOL_PREFIXES.some((prefix) => normalized.startsWith(prefix));
}

function isWithinWorkIntervals(timestamp: Date, intervals: TaskWorkInterval[]): boolean {
  if (!Number.isFinite(timestamp.getTime())) {
    return false;
  }
  if (intervals.length === 0) {
    return true;
  }

  const time = timestamp.getTime();
  return intervals.some((interval) => {
    const startedAt = Date.parse(interval.startedAt);
    if (!Number.isFinite(startedAt) || time < startedAt) {
      return false;
    }
    if (interval.completedAt === undefined) {
      return true;
    }
    const completedAt = Date.parse(interval.completedAt);
    const endMs = Number.isFinite(completedAt) ? Math.max(completedAt, startedAt) : startedAt;
    return time <= endMs;
  });
}

function pushUnique(values: string[], value: string | undefined): void {
  if (!value) return;
  if (!values.includes(value)) {
    values.push(value);
  }
}

function pushExample(
  examples: BoardTaskLogDiagnosticExample[],
  example: BoardTaskLogDiagnosticExample
): void {
  if (examples.length < MAX_EXAMPLES) {
    examples.push(example);
  }
}

function buildParticipantLabel(record: BoardTaskActivityRecord): string {
  if (record.actor.memberName) {
    return record.actor.memberName;
  }
  if (!record.actor.isSidechain || record.actor.role === 'lead') {
    return 'lead session';
  }
  if (record.actor.agentId) {
    return `member ${record.actor.agentId.slice(0, 8)}`;
  }
  return `member session ${record.actor.sessionId.slice(0, 8)}`;
}

function extractVisibleToolNames(
  stream: Awaited<ReturnType<BoardTaskLogStreamService['getTaskLogStream']>>
): string[] {
  const toolNames: string[] = [];
  for (const segment of stream.segments) {
    for (const chunk of segment.chunks) {
      for (const message of chunk.rawMessages) {
        for (const toolCall of message.toolCalls) {
          pushUnique(toolNames, toolCall.name);
        }
      }
    }
  }
  return toolNames;
}

function buildStreamToolNameMap(
  stream: Awaited<ReturnType<BoardTaskLogStreamService['getTaskLogStream']>>
): Map<string, string> {
  const toolNameByUseId = new Map<string, string>();
  for (const segment of stream.segments) {
    for (const chunk of segment.chunks) {
      for (const message of chunk.rawMessages) {
        for (const toolCall of message.toolCalls) {
          toolNameByUseId.set(toolCall.id, toolCall.name);
        }
      }
    }
  }
  return toolNameByUseId;
}

function isEmptyToolPayload(value: unknown): boolean {
  if (value == null) return true;
  if (typeof value === 'string') {
    return value.trim().length === 0;
  }
  if (Array.isArray(value)) {
    return value.length === 0;
  }
  return false;
}

function asObjectRecord(value: unknown): Record<string, unknown> | null {
  return value && typeof value === 'object' && !Array.isArray(value)
    ? (value as Record<string, unknown>)
    : null;
}

function collectEmptyPayloadExamples(
  stream: Awaited<ReturnType<BoardTaskLogStreamService['getTaskLogStream']>>
): BoardTaskLogDiagnosticExample[] {
  const examples: BoardTaskLogDiagnosticExample[] = [];
  const toolNameByUseId = buildStreamToolNameMap(stream);

  for (const segment of stream.segments) {
    for (const chunk of segment.chunks) {
      for (const message of chunk.rawMessages) {
        for (const toolResult of message.toolResults) {
          if (!isEmptyToolPayload(toolResult.content)) {
            continue;
          }
          pushExample(examples, {
            timestamp: message.timestamp.toISOString(),
            filePath: 'stream',
            messageUuid: message.uuid,
            toolUseId: toolResult.toolUseId,
            toolName: toolNameByUseId.get(toolResult.toolUseId) ?? 'unknown tool',
            isSidechain: message.isSidechain,
            ...(message.agentId ? { agentId: message.agentId } : {}),
          });
        }

        const toolUseResult = asObjectRecord(message.toolUseResult);
        if (!toolUseResult) {
          continue;
        }
        const toolUseId =
          typeof toolUseResult.toolUseId === 'string'
            ? toolUseResult.toolUseId
            : message.sourceToolUseID;
        const contentIsEmpty =
          (!('content' in toolUseResult) || isEmptyToolPayload(toolUseResult.content)) &&
          (!('message' in toolUseResult) || isEmptyToolPayload(toolUseResult.message));
        if (!contentIsEmpty) {
          continue;
        }

        pushExample(examples, {
          timestamp: message.timestamp.toISOString(),
          filePath: 'stream',
          messageUuid: message.uuid,
          ...(toolUseId ? { toolUseId } : {}),
          toolName: toolUseId ? (toolNameByUseId.get(toolUseId) ?? 'unknown tool') : 'unknown tool',
          isSidechain: message.isSidechain,
          ...(message.agentId ? { agentId: message.agentId } : {}),
        });
      }
    }
  }

  return examples;
}

function buildToolNameMap(parsedMessagesByFile: Map<string, ParsedMessage[]>): Map<string, string> {
  const toolNameByUseId = new Map<string, string>();
  for (const messages of parsedMessagesByFile.values()) {
    for (const message of messages) {
      for (const toolCall of message.toolCalls) {
        toolNameByUseId.set(toolCall.id, toolCall.name);
      }
    }
  }
  return toolNameByUseId;
}

export class BoardTaskLogDiagnosticsService {
  constructor(
    private readonly taskReader: TeamTaskReader = new TeamTaskReader(),
    private readonly transcriptSourceLocator: TeamTranscriptSourceLocator = new TeamTranscriptSourceLocator(),
    private readonly recordSource: BoardTaskActivityRecordSource = new BoardTaskActivityRecordSource(),
    private readonly strictParser: BoardTaskExactLogStrictParser = new BoardTaskExactLogStrictParser(),
    private readonly streamService: BoardTaskLogStreamService = new BoardTaskLogStreamService(),
    private readonly transcriptCandidateSelector: TaskLogTranscriptCandidateSelector = new TaskLogTranscriptCandidateSelector(),
    private readonly historicalRawProbe: HistoricalBoardMcpRawProbe = new HistoricalBoardMcpRawProbe()
  ) {}

  private async getTranscriptContext(teamName: string): Promise<{
    projectDir?: string;
    transcriptFiles: string[];
  }> {
    const locator = this.transcriptSourceLocator as {
      getContext?: (
        teamName: string
      ) => Promise<{ projectDir: string; transcriptFiles: string[] } | null>;
      listTranscriptFiles?: (teamName: string) => Promise<string[]>;
    };
    const context = await locator.getContext?.(teamName);
    if (context) {
      return {
        projectDir: context.projectDir,
        transcriptFiles: context.transcriptFiles,
      };
    }
    return {
      transcriptFiles: (await locator.listTranscriptFiles?.(teamName)) ?? [],
    };
  }

  private async parseDiagnosticCandidateFiles(args: {
    task: TeamTask;
    records: BoardTaskActivityRecord[];
    projectDir?: string;
    transcriptFiles: string[];
  }): Promise<{
    parsedMessagesByFile: Map<string, ParsedMessage[]>;
    candidateSelection: NonNullable<
      BoardTaskLogDiagnosticsReport['transcript']['candidateSelection']
    >;
  }> {
    if (args.transcriptFiles.length === 0) {
      return {
        parsedMessagesByFile: new Map(),
        candidateSelection: {
          mode: 'none',
          candidateFileCount: 0,
        },
      };
    }

    if (args.records.length > 0) {
      const selection = this.transcriptCandidateSelector.selectInferredNativeTranscriptFiles({
        records: args.records,
        transcriptFiles: args.transcriptFiles,
        projectDir: args.projectDir,
      });
      return {
        parsedMessagesByFile:
          selection.filePaths.length > 0
            ? await this.strictParser.parseFiles(selection.filePaths)
            : new Map(),
        candidateSelection: {
          mode: 'activity_records',
          candidateFileCount: selection.filePaths.length,
        },
      };
    }

    const rawProbe = await this.historicalRawProbe.findCandidateFiles({
      task: args.task,
      transcriptFiles: args.transcriptFiles,
    });
    return {
      parsedMessagesByFile:
        rawProbe.filePaths.length > 0
          ? await this.strictParser.parseFiles(rawProbe.filePaths)
          : new Map(),
      candidateSelection: {
        mode: 'historical_raw_probe',
        candidateFileCount: rawProbe.filePaths.length,
        rawProbeScannedFileCount: rawProbe.scannedFileCount,
        rawProbeHitCount: rawProbe.hitCount,
      },
    };
  }

  async diagnose(teamName: string, taskRef: string): Promise<BoardTaskLogDiagnosticsReport> {
    const normalizedRef = normalizeRequestedTaskRef(taskRef);
    const [activeTasks, deletedTasks, transcriptContext] = await Promise.all([
      this.taskReader.getTasks(teamName),
      this.taskReader.getDeletedTasks(teamName),
      this.getTranscriptContext(teamName),
    ]);

    const tasks = [...activeTasks, ...deletedTasks];
    const task = tasks.find((candidate) => taskMatchesRef(candidate, normalizedRef));
    if (!task) {
      throw new Error(`Task "${taskRef}" was not found in team "${teamName}"`);
    }

    const records = await this.recordSource.getTaskRecords(teamName, task.id);
    const { parsedMessagesByFile, candidateSelection } = await this.parseDiagnosticCandidateFiles({
      task,
      records,
      projectDir: transcriptContext.projectDir,
      transcriptFiles: transcriptContext.transcriptFiles,
    });
    const stream = await this.streamService.getTaskLogStream(teamName, task.id);

    const toolNameByUseId = buildToolNameMap(parsedMessagesByFile);
    const explicitExecutionKeys = new Set(
      records
        .filter((record) => record.linkKind === 'execution')
        .map((record) => `${record.source.messageUuid}:${record.source.toolUseId ?? ''}`)
    );
    const workIntervals = Array.isArray(task.workIntervals) ? task.workIntervals : [];

    const explicitParticipants: string[] = [];
    const explicitToolNames: string[] = [];
    for (const record of records) {
      pushUnique(explicitParticipants, buildParticipantLabel(record));
      pushUnique(explicitToolNames, record.action?.canonicalToolName);
    }

    let intervalToolResultTotal = 0;
    let boardMcpToolResultTotal = 0;
    let workerToolResultTotal = 0;
    let explicitLinkedWorkerResultTotal = 0;
    let missingExplicitWorkerResultTotal = 0;
    const missingExplicitWorkerExamples: BoardTaskLogDiagnosticExample[] = [];

    for (const [filePath, messages] of parsedMessagesByFile.entries()) {
      for (const message of messages) {
        if (message.type !== 'user' || message.toolResults.length === 0) {
          continue;
        }
        if (!isWithinWorkIntervals(message.timestamp, workIntervals)) {
          continue;
        }

        for (const toolResult of message.toolResults) {
          intervalToolResultTotal += 1;
          const toolName = toolNameByUseId.get(toolResult.toolUseId) ?? 'unknown tool';
          if (isBoardMcpToolName(toolName)) {
            boardMcpToolResultTotal += 1;
            continue;
          }

          workerToolResultTotal += 1;
          const explicitKey = `${message.uuid}:${toolResult.toolUseId}`;
          if (explicitExecutionKeys.has(explicitKey)) {
            explicitLinkedWorkerResultTotal += 1;
            continue;
          }

          missingExplicitWorkerResultTotal += 1;
          pushExample(missingExplicitWorkerExamples, {
            timestamp: message.timestamp.toISOString(),
            filePath,
            messageUuid: message.uuid,
            toolUseId: toolResult.toolUseId,
            toolName,
            isSidechain: message.isSidechain,
            ...(message.agentId ? { agentId: message.agentId } : {}),
          });
        }
      }
    }

    const diagnosis: string[] = [];
    if (transcriptContext.transcriptFiles.length === 0) {
      diagnosis.push('No transcript files were found for this team.');
    }
    if (records.length === 0) {
      diagnosis.push('No explicit task-linked activity records were found for this task.');
    }
    if (missingExplicitWorkerResultTotal > 0) {
      diagnosis.push(
        `Only board MCP actions are explicit for part of this task history. Found ${missingExplicitWorkerResultTotal} worker tool result(s) inside task work intervals without boardTaskLinks, so Task Log Stream cannot safely include them.`
      );
    }
    if (
      missingExplicitWorkerResultTotal > 0 &&
      extractVisibleToolNames(stream).every((toolName) => isBoardMcpToolName(toolName))
    ) {
      diagnosis.push(
        'Current stream visibility matches the data gap: the visible tools are MCP board actions, while worker tools exist in transcript but are unlinked.'
      );
    }

    const emptyPayloadExamples = collectEmptyPayloadExamples(stream);
    if (emptyPayloadExamples.length > 0) {
      diagnosis.push(
        `Found ${emptyPayloadExamples.length} tool result payload(s) with empty rendered content in the current stream. This explains empty success/output blocks.`
      );
    }
    if (diagnosis.length === 0) {
      diagnosis.push('No obvious task-log data gap was detected by diagnostics.');
    }

    return {
      teamName,
      requestedTaskRef: taskRef,
      task: {
        taskId: task.id,
        displayId: getTaskDisplayId(task),
        subject: task.subject,
        status: task.status,
        ...(task.owner ? { owner: task.owner } : {}),
        workIntervals,
      },
      transcript: {
        fileCount: transcriptContext.transcriptFiles.length,
        files: transcriptContext.transcriptFiles,
        parsedFileCount: parsedMessagesByFile.size,
        candidateSelection,
      },
      explicitRecords: {
        total: records.length,
        execution: records.filter((record) => record.linkKind === 'execution').length,
        lifecycle: records.filter((record) => record.linkKind === 'lifecycle').length,
        boardAction: records.filter((record) => record.linkKind === 'board_action').length,
        participants: explicitParticipants,
        toolNames: explicitToolNames,
      },
      intervalToolResults: {
        total: intervalToolResultTotal,
        boardMcp: boardMcpToolResultTotal,
        worker: {
          total: workerToolResultTotal,
          explicitLinked: explicitLinkedWorkerResultTotal,
          missingExplicit: missingExplicitWorkerResultTotal,
          examples: missingExplicitWorkerExamples,
        },
      },
      stream: {
        participants: stream.participants.map((participant) => participant.label),
        defaultFilter: stream.defaultFilter,
        segmentCount: stream.segments.length,
        visibleToolNames: extractVisibleToolNames(stream),
        emptyPayloadExamples,
      },
      diagnosis,
    };
  }
}
