/**
 * Transforms EnhancedChunk[] into SessionConversation structure.
 *
 * This module converts chunk-based data into a flat list of ChatItems
 * (UserGroups, SystemGroups, AIGroups) for a chat-style display.
 * Each item is independent - no pairing between user and AI chunks.
 */

import {
  isAssistantMessage,
  isEnhancedAIChunk,
  isEnhancedCompactChunk,
  isEnhancedSystemChunk,
  isEnhancedUserChunk,
} from '@renderer/types/data';
import {
  getFirstSegment,
  hasPathSeparator,
  isRelativePath,
  splitPathSegments,
} from '@renderer/utils/pathUtils';
import { isCommandContent, sanitizeDisplayContent } from '@shared/utils/contentSanitizer';
import { createLogger } from '@shared/utils/logger';

import type {
  EnhancedAIChunk,
  EnhancedChunk,
  EnhancedCompactChunk,
  EnhancedSystemChunk,
  EnhancedUserChunk,
  ParsedMessage,
  Process,
  SemanticStep,
} from '@renderer/types/data';
import type {
  AIGroup,
  AIGroupStatus,
  AIGroupSummary,
  AIGroupTokens,
  ChatItem,
  CommandInfo,
  CompactGroup,
  FileReference,
  ImageData,
  SessionConversation,
  SystemGroup,
  UserGroup,
  UserGroupContent,
} from '@renderer/types/groups';

const logger = createLogger('Util:groupTransformer');

// =============================================================================
// Constants
// =============================================================================

/**
 * Regex pattern for detecting slash commands.
 * Matches: /command-name [optional args]
 * Uses non-greedy matching and limited repetition to prevent ReDoS.
 */
// eslint-disable-next-line security/detect-unsafe-regex -- Pattern is safe: limited to 1000 chars and used on bounded user input
const COMMAND_PATTERN = /\/([a-z][a-z-]{0,50})(?:\s+(\S[^\n]{0,1000}))?$/gim;

/**
 * Maximum characters to extract for thinking preview.
 */
const THINKING_PREVIEW_LENGTH = 100;

// =============================================================================
// Main Transformation Function
// =============================================================================

/**
 * Transforms EnhancedChunk[] into SessionConversation.
 *
 * Produces a flat list of independent ChatItems (user, system, AI).
 * Each chunk type becomes its own item - no pairing or grouping.
 *
 * @param chunks - Array of enhanced chunks with semantic steps
 * @param _subagents - Array of all subagents in the session (unused, processes come from chunks)
 * @param isOngoing - Whether the session is still in progress (marks last AI group)
 * @returns SessionConversation structure for chat-style rendering
 */
export function transformChunksToConversation(
  chunks: EnhancedChunk[],
  _subagents: Process[],
  isOngoing: boolean = false
): SessionConversation {
  if (!chunks || chunks.length === 0) {
    return {
      sessionId: '',
      items: [],
      totalUserGroups: 0,
      totalSystemGroups: 0,
      totalAIGroups: 0,
      totalCompactGroups: 0,
    };
  }

  const items: ChatItem[] = [];
  let userCount = 0;
  let systemCount = 0;
  let aiCount = 0;
  let compactCount = 0;

  for (const chunk of chunks) {
    if (isEnhancedUserChunk(chunk)) {
      items.push({
        type: 'user',
        group: createUserGroupFromChunk(chunk, userCount++),
      });
    } else if (isEnhancedSystemChunk(chunk)) {
      items.push({
        type: 'system',
        group: createSystemGroup(chunk),
      });
      systemCount++;
    } else if (isEnhancedAIChunk(chunk)) {
      items.push({
        type: 'ai',
        group: createAIGroupFromChunk(chunk, aiCount),
      });
      aiCount++;
    } else if (isEnhancedCompactChunk(chunk)) {
      items.push({
        type: 'compact',
        group: createCompactGroup(chunk),
      });
      compactCount++;
    } else {
      const unhandledChunkType =
        'chunkType' in chunk ? (chunk as EnhancedChunk).chunkType : 'unknown';
      logger.warn('Unhandled chunk type:', unhandledChunkType);
    }
  }

  // Post-pass: enrich CompactGroups with token deltas
  let phaseCounter = 1;
  for (let i = 0; i < items.length; i++) {
    if (items[i].type === 'compact') {
      phaseCounter++;
      const compactItem = items[i] as { type: 'compact'; group: CompactGroup };
      compactItem.group.startingPhaseNumber = phaseCounter;

      // Find last AI group before and first AI group after
      const preAi = findLastAiBefore(items, i);
      const postAi = findFirstAiAfter(items, i);
      if (preAi && postAi) {
        const pre = getLastAssistantTotalTokens(preAi);
        // Use FIRST assistant message after compaction — it reflects the actual
        // compacted context size before the AI generates more content.
        const post = getFirstAssistantTotalTokens(postAi);
        if (pre !== undefined && post !== undefined) {
          compactItem.group.tokenDelta = {
            preCompactionTokens: pre,
            postCompactionTokens: post,
            delta: post - pre,
          };
        }
      }
    }
  }

  // If session is ongoing, mark the last AI group (but don't override interrupted status)
  if (isOngoing && aiCount > 0) {
    // Find the last AI item and mark it as ongoing
    for (let i = items.length - 1; i >= 0; i--) {
      const item = items[i];
      if (item.type === 'ai') {
        const currentStatus = item.group.status;
        // Don't override 'interrupted' status - interruption takes precedence over ongoing
        if (currentStatus !== 'interrupted') {
          (item.group as AIGroup & { isOngoing?: boolean }).isOngoing = true;
          (item.group as AIGroup & { status?: AIGroupStatus }).status = 'in_progress';
        }
        break;
      }
    }
  }

  return {
    sessionId: chunks[0]?.id ?? 'unknown',
    items,
    totalUserGroups: userCount,
    totalSystemGroups: systemCount,
    totalAIGroups: aiCount,
    totalCompactGroups: compactCount,
  };
}

// =============================================================================
// UserGroup Creation
// =============================================================================

/**
 * Creates a UserGroup from an EnhancedUserChunk.
 *
 * @param chunk - The user chunk to transform
 * @param index - Index within the session (for ordering)
 * @returns UserGroup with parsed content
 */
function createUserGroupFromChunk(chunk: EnhancedUserChunk, index: number): UserGroup {
  return createUserGroup(chunk.userMessage, index);
}

/**
 * Creates a UserGroup from a ParsedMessage.
 *
 * @param message - The user's input message
 * @param index - Index within the session (for ordering)
 * @returns UserGroup with parsed content
 */
function createUserGroup(message: ParsedMessage, index: number): UserGroup {
  const content = extractUserGroupContent(message);

  return {
    id: `user-${message.uuid}`,
    message,
    timestamp: message.timestamp,
    content,
    index,
  };
}

/**
 * Extracts and parses content from a user message.
 *
 * @param message - The user message to parse
 * @returns Parsed UserGroupContent
 */
function extractUserGroupContent(message: ParsedMessage): UserGroupContent {
  let rawText = '';
  const images: ImageData[] = [];
  const fileReferences: FileReference[] = [];

  // Extract text from content
  // Note: Image handling not yet implemented - images are not part of ContentBlock type
  if (typeof message.content === 'string') {
    rawText = message.content;
  } else if (Array.isArray(message.content)) {
    for (const block of message.content) {
      if (block.type === 'text' && block.text) {
        rawText += block.text;
      }
    }
  }

  // Sanitize content for display (handles XML tags from command messages)
  // This converts <command-name>/model</command-name> to "/model"
  const sanitizedText = sanitizeDisplayContent(rawText);

  // Check if this is a command message (for special handling)
  const isCommand = isCommandContent(rawText);

  // Extract commands from the sanitized text (for inline /commands in regular messages)
  // For command messages, the command is already extracted as sanitizedText
  const commands = isCommand ? [] : extractCommands(sanitizedText);

  // Extract file references (@file.ts) from sanitized text
  fileReferences.push(...extractFileReferences(sanitizedText));

  // For command messages, use the sanitized command as display text
  // For regular messages, remove inline commands from display
  let displayText = sanitizedText;
  if (!isCommand) {
    for (const cmd of commands) {
      displayText = displayText.replace(cmd.raw, '').trim();
    }
  }

  return {
    text: displayText || undefined,
    rawText: sanitizedText, // Use sanitized version as rawText for display
    commands,
    images,
    fileReferences,
  };
}

/**
 * Extracts commands from text using regex.
 *
 * @param text - Text to parse for commands
 * @returns Array of CommandInfo objects
 */
function extractCommands(text: string): CommandInfo[] {
  if (!text) return [];

  const commands: CommandInfo[] = [];
  let match: RegExpExecArray | null;

  // Reset regex state
  COMMAND_PATTERN.lastIndex = 0;

  while ((match = COMMAND_PATTERN.exec(text)) !== null) {
    const [fullMatch, commandName, args] = match;
    commands.push({
      name: commandName,
      args: args?.trim(),
      raw: fullMatch,
      startIndex: match.index,
      endIndex: match.index + fullMatch.length,
    });
  }

  return commands;
}

/**
 * Known directory prefixes that identify file references.
 */
const KNOWN_DIRS = new Set([
  'src',
  'apps',
  'app',
  'lib',
  'types',
  'packages',
  'components',
  'utils',
  'services',
  'hooks',
  'store',
  'public',
  'assets',
  'config',
  'tests',
  'test',
  'specs',
  'spec',
  'e2e',
  'docs',
  'scripts',
  'screens',
  'features',
  'pages',
  'views',
  'models',
  'controllers',
  'routes',
  'middleware',
  'api',
  'common',
  'core',
  'modules',
  'client',
  'server',
  'web',
  'mobile',
  'native',
  'electron',
  'node_modules',
]);

export type FileReferenceToken = FileReference & {
  startIndex: number;
  endIndex: number;
};

const UNQUOTED_FILE_REF_STOP = /[\s,)}\]]/;

/**
 * Checks if a path looks like a valid file reference.
 * Must start with known dir, contain /, or start with ./ or ../
 */
function isValidFileRef(path: string): boolean {
  if (/^[A-Za-z]:[\\/]/.test(path) || path.startsWith('\\\\')) {
    return true;
  }
  // Check for relative path indicators
  if (isRelativePath(path)) {
    return true;
  }
  // Check if starts with known directory
  const first = getFirstSegment(path);
  if (KNOWN_DIRS.has(first)) {
    return true;
  }
  // Check if contains a path separator (indicates directory structure)
  // Require either 3+ segments or a file extension to avoid matching
  // npm scoped packages like @scope/name (extracted as "scope/name")
  if (hasPathSeparator(path) && path.length > 2) {
    const segments = splitPathSegments(path);
    if (segments.length > 2 || /\.[a-zA-Z0-9]+$/.test(path)) {
      return true;
    }
  }
  return false;
}

function readFileRefAt(text: string, atIndex: number): FileReferenceToken | null {
  const valueStart = atIndex + 1;
  const firstChar = text[valueStart];
  if (!firstChar) return null;

  let path = '';
  let endIndex = valueStart;

  if (firstChar === '"' || firstChar === "'") {
    const quote = firstChar;
    const quotedStart = valueStart + 1;
    const quotedEnd = text.indexOf(quote, quotedStart);
    if (quotedEnd < 0) return null;
    path = text.slice(quotedStart, quotedEnd);
    endIndex = quotedEnd + 1;
  } else {
    while (endIndex < text.length && !UNQUOTED_FILE_REF_STOP.test(text[endIndex])) {
      endIndex += 1;
    }
    path = text.slice(valueStart, endIndex);
  }

  if (!path || !isValidFileRef(path)) return null;
  return {
    path,
    raw: text.slice(atIndex, endIndex),
    startIndex: atIndex,
    endIndex,
  };
}

export function extractFileReferenceTokens(text: string): FileReferenceToken[] {
  if (!text) return [];

  const references: FileReferenceToken[] = [];
  let index = 0;
  while (index < text.length) {
    const atIndex = text.indexOf('@', index);
    if (atIndex < 0) break;

    const reference = readFileRefAt(text, atIndex);
    if (reference) {
      references.push(reference);
      index = reference.endIndex;
    } else {
      index = atIndex + 1;
    }
  }

  return references;
}

/**
 * Extracts file references (@file.ts) from text.
 *
 * @param text - Text to parse for file references
 * @returns Array of FileReference objects
 */
export function extractFileReferences(text: string): FileReference[] {
  return extractFileReferenceTokens(text).map(({ path, raw }) => ({ path, raw }));
}

// =============================================================================
// SystemGroup Creation
// =============================================================================

/**
 * Creates a SystemGroup from an EnhancedSystemChunk.
 *
 * @param chunk - The system chunk to transform
 * @returns SystemGroup with command output
 */
function createSystemGroup(chunk: EnhancedSystemChunk): SystemGroup {
  return {
    id: chunk.id, // Use stable chunk ID instead of array index
    message: chunk.message,
    timestamp: chunk.startTime,
    commandOutput: chunk.commandOutput,
  };
}

// =============================================================================
// CompactGroup Creation
// =============================================================================

/**
 * Creates a CompactGroup from an EnhancedCompactChunk.
 *
 * @param chunk - The compact chunk to transform
 * @returns CompactGroup marking where conversation was compacted, with message content
 */
function createCompactGroup(chunk: EnhancedCompactChunk): CompactGroup {
  return {
    id: chunk.id, // Use stable chunk ID instead of array index
    timestamp: chunk.startTime,
    message: chunk.message, // Pass through the compact summary message
  };
}

// =============================================================================
// AIGroup Creation
// =============================================================================

/**
 * Creates an AIGroup from an EnhancedAIChunk.
 *
 * @param chunk - The AI chunk to transform
 * @param turnIndex - 0-based index of this AI group within the session
 * @returns AIGroup with semantic steps and metrics
 */
function createAIGroupFromChunk(chunk: EnhancedAIChunk, turnIndex: number): AIGroup {
  const steps = chunk.semanticSteps;

  // Calculate timing from all steps
  const startTime = steps.length > 0 ? steps[0].startTime : chunk.startTime;
  const endTime =
    steps.length > 0
      ? (steps[steps.length - 1].endTime ?? steps[steps.length - 1].startTime)
      : chunk.endTime;
  const durationMs = endTime.getTime() - startTime.getTime();

  // Find any source assistant message for token calculation
  const sourceMessage = chunk.responses.find((msg) => isAssistantMessage(msg)) ?? null;

  // Calculate tokens from all steps
  const tokens = calculateTokensFromSteps(steps, sourceMessage);

  // Generate summary from all steps
  const summary = computeAIGroupSummary(steps);

  // Determine status from all steps
  const status = determineAIGroupStatus(steps);

  return {
    id: chunk.id, // Use stable chunk ID instead of array index
    turnIndex,
    startTime,
    endTime,
    durationMs,
    steps,
    tokens,
    summary,
    status,
    processes: chunk.processes,
    chunkId: chunk.id,
    metrics: chunk.metrics,
    responses: chunk.responses,
  };
}

/**
 * Calculates token metrics from semantic steps and source message.
 *
 * @param steps - Semantic steps in this AI Group
 * @param sourceMessage - Source assistant message (if available)
 * @returns Token metrics
 */
function calculateTokensFromSteps(
  steps: SemanticStep[],
  sourceMessage: ParsedMessage | null | undefined
): AIGroupTokens {
  let input = 0;
  let output = 0;
  let cached = 0;
  let thinking = 0;

  // Sum from steps
  for (const step of steps) {
    if (step.tokens) {
      input += step.tokens.input ?? 0;
      output += step.tokens.output ?? 0;
      cached += step.tokens.cached ?? 0;
    }
    if (step.tokenBreakdown) {
      input += step.tokenBreakdown.input ?? 0;
      output += step.tokenBreakdown.output ?? 0;
      cached += step.tokenBreakdown.cacheRead ?? 0;
    }
    if (step.type === 'thinking' && step.tokens?.output) {
      thinking += step.tokens.output;
    }
  }

  // Override with source message usage if available (more accurate)
  if (sourceMessage?.usage) {
    input = sourceMessage.usage.input_tokens ?? 0;
    output = sourceMessage.usage.output_tokens ?? 0;
    cached = sourceMessage.usage.cache_read_input_tokens ?? 0;
  }

  return {
    input,
    output,
    cached,
    thinking,
  };
}

// =============================================================================
// AIGroup Summary & Status Computation
// =============================================================================

/**
 * Computes summary statistics for an AIGroup's collapsed view.
 *
 * @param steps - Semantic steps in the AI Group
 * @returns Summary statistics
 */
function computeAIGroupSummary(steps: SemanticStep[]): AIGroupSummary {
  let thinkingPreview: string | undefined;
  let toolCallCount = 0;
  let outputMessageCount = 0;
  let subagentCount = 0;
  let totalDurationMs = 0;
  let totalTokens = 0;
  let outputTokens = 0;
  let cachedTokens = 0;

  for (const step of steps) {
    // Extract thinking preview from first thinking step
    if (!thinkingPreview && step.type === 'thinking' && step.content.thinkingText) {
      const fullText = step.content.thinkingText;
      thinkingPreview =
        fullText.length > THINKING_PREVIEW_LENGTH
          ? fullText.slice(0, THINKING_PREVIEW_LENGTH) + '...'
          : fullText;
    }

    // Count step types
    if (step.type === 'tool_call') toolCallCount++;
    if (step.type === 'output') outputMessageCount++;
    if (step.type === 'subagent') subagentCount++;

    // Sum duration
    totalDurationMs += step.durationMs ?? 0;

    // Sum tokens
    if (step.tokens) {
      totalTokens += (step.tokens.input ?? 0) + (step.tokens.output ?? 0);
      outputTokens += step.tokens.output ?? 0;
      cachedTokens += step.tokens.cached ?? 0;
    }
    if (step.tokenBreakdown) {
      totalTokens += step.tokenBreakdown.input + step.tokenBreakdown.output;
      outputTokens += step.tokenBreakdown.output;
      cachedTokens += step.tokenBreakdown.cacheRead;
    }
  }

  return {
    thinkingPreview,
    toolCallCount,
    outputMessageCount,
    subagentCount,
    totalDurationMs,
    totalTokens,
    outputTokens,
    cachedTokens,
  };
}

/**
 * Determines the status of an AIGroup based on its steps.
 *
 * @param steps - Semantic steps in the AI Group
 * @returns AIGroupStatus
 */
function determineAIGroupStatus(steps: SemanticStep[]): AIGroupStatus {
  if (steps.length === 0) return 'error';

  // Check for interruption
  const hasInterruption = steps.some((step) => step.type === 'interruption');
  if (hasInterruption) return 'interrupted';

  // Check for errors
  const hasError = steps.some((step) => step.type === 'tool_result' && step.content.isError);
  if (hasError) return 'error';

  // Check if any step is incomplete (no endTime)
  const hasIncomplete = steps.some((step) => !step.endTime);
  if (hasIncomplete) return 'in_progress';

  // Otherwise, complete
  return 'complete';
}

// =============================================================================
// CompactGroup Enrichment Helpers
// =============================================================================

/**
 * Find the last AI group before a given index in the items array.
 */
function findLastAiBefore(items: ChatItem[], index: number): AIGroup | null {
  for (let i = index - 1; i >= 0; i--) {
    if (items[i].type === 'ai') return items[i].group as AIGroup;
  }
  return null;
}

/**
 * Find the first AI group after a given index in the items array.
 */
function findFirstAiAfter(items: ChatItem[], index: number): AIGroup | null {
  for (let i = index + 1; i < items.length; i++) {
    if (items[i].type === 'ai') return items[i].group as AIGroup;
  }
  return null;
}

/**
 * Get total tokens from the last assistant message in an AI group.
 * Sums input_tokens, output_tokens, cache_read_input_tokens, and cache_creation_input_tokens.
 */
function getLastAssistantTotalTokens(aiGroup: AIGroup): number | undefined {
  const responses = aiGroup.responses || [];
  for (let i = responses.length - 1; i >= 0; i--) {
    const msg = responses[i];
    if (msg.type === 'assistant' && msg.usage) {
      return (
        (msg.usage.input_tokens ?? 0) +
        (msg.usage.output_tokens ?? 0) +
        (msg.usage.cache_read_input_tokens ?? 0) +
        (msg.usage.cache_creation_input_tokens ?? 0)
      );
    }
  }
  return undefined;
}

/**
 * Get total tokens from the FIRST assistant message in an AI group.
 * Used for post-compaction token measurement: the first response after compaction
 * reflects the actual compacted context size before the AI generates more content.
 */
function getFirstAssistantTotalTokens(aiGroup: AIGroup): number | undefined {
  const responses = aiGroup.responses || [];
  for (const msg of responses) {
    if (msg.type === 'assistant' && msg.usage) {
      return (
        (msg.usage.input_tokens ?? 0) +
        (msg.usage.output_tokens ?? 0) +
        (msg.usage.cache_read_input_tokens ?? 0) +
        (msg.usage.cache_creation_input_tokens ?? 0)
      );
    }
  }
  return undefined;
}

// =============================================================================
// Helper Functions
// =============================================================================
