/**
 * @license
 * Copyright 2026 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */

import { type Part, type Content } from '@google/genai';
import { debugLogger } from './debugLogger.js';
import { type HistoryTurn } from '../core/agentChatHistory.js';
import { deriveStableId } from './cryptoUtils.js';

export const SYNTHETIC_THOUGHT_SIGNATURE = 'skip_thought_signature_validator';

export interface HardeningOptions {
  sentinels?: {
    continuation?: string;
    lostToolResponse?: string;
  };
}

const DEFAULT_SENTINELS = {
  continuation: '[Continuing from previous AI thoughts...]',
  lostToolResponse:
    'The tool execution result was lost due to context management truncation.',
};

/**
 * Hardens a chat history to ensure it strictly adheres to Gemini API invariants.
 * This is a defensive post-processing pass that patches violations using
 * sentinel messages rather than failing.
 *
 * Invariants enforced:
 * 1. Role Alternation: user -> model -> user -> model
 * 2. Start Constraint: Must start with a 'user' turn.
 * 3. End Constraint: Must end with a 'user' turn (usually for follow-up prompts).
 * 4. Tool Pairing: Every model functionCall must be followed by a user functionResponse.
 * 5. Signatures: The first functionCall in a model turn must have a thoughtSignature.
 */
export function hardenHistory(
  history: HistoryTurn[],
  options: HardeningOptions = {},
): HistoryTurn[] {
  if (history.length === 0) return history;

  const sentinels = { ...DEFAULT_SENTINELS, ...options.sentinels };

  // Pass 0: Strip internal thoughts and remove empty turns
  const processed = stripThoughts(history);

  // Pass 1: Initial Coalesce & Empty Turn Removal
  let coalesced = coalesce(processed);

  // Pass 2: Tool Pairing & Signatures (The semantic layer)
  coalesced = pairToolsAndEnforceSignatures(coalesced, sentinels);

  // Pass 3: Structural Refinement (Hoisting & Re-ordering of tool responses)
  coalesced = refineToolResponses(coalesced);

  // Pass 4: Enforce Structural Invariants (Start/End/Alternation)
  let final = enforceRoleConstraints(coalesced, sentinels);

  // Pass 5: Final Scrubbing (Remove custom/non-standard properties for API compatibility)
  final = scrubHistory(final);

  return final;
}

/**
 * Helper to check if a Part object represents an internal thought.
 */
function isInternalThought(part: Part): boolean {
  return !!part && !!(part as ThoughtPart).thought;
}

/**
 * Removes parts that represent thoughts (where part.thought === true).
 * Empty turns resulting from thought removal are handled in subsequent coalescing passes.
 */
function stripThoughts(history: HistoryTurn[]): HistoryTurn[] {
  return history.map((turn) => {
    if (!turn.content.parts) return turn;
    const hasThought = turn.content.parts.some(isInternalThought);
    if (!hasThought) return turn;

    const nonThoughtParts = turn.content.parts.filter(
      (p) => p && !isInternalThought(p),
    );
    return {
      id: turn.id,
      content: {
        ...turn.content,
        parts: nonThoughtParts,
      },
    };
  });
}

/**
 * Combines adjacent turns with the same role and removes empty turns.
 */
function coalesce(history: HistoryTurn[]): HistoryTurn[] {
  const result: HistoryTurn[] = [];
  for (const turn of history) {
    if (!turn.content.parts || turn.content.parts.length === 0) continue;

    const lastIdx = result.length - 1;
    const last = result[lastIdx];
    if (last && last.content.role === turn.content.role) {
      result[lastIdx] = {
        id: last.id,
        content: {
          ...last.content,
          parts: [...(last.content.parts || []), ...(turn.content.parts || [])],
        },
      };
    } else {
      // Shallow clone the turn and content so we don't mutate the original history array structure
      result.push({ id: turn.id, content: { ...turn.content } });
    }
  }
  return result;
}

/**
 * Ensures tool calls have matching responses and model turns have required signatures.
 */
function pairToolsAndEnforceSignatures(
  history: HistoryTurn[],
  sentinels: Required<NonNullable<HardeningOptions['sentinels']>>,
): HistoryTurn[] {
  const result: HistoryTurn[] = [];

  // We work on a copy to allow splicing in sentinel turns
  const work = [...history];

  for (let i = 0; i < work.length; i++) {
    const turn = work[i];

    if (turn.content.role === 'model') {
      const parts = turn.content.parts || [];

      // A. Signatures
      let foundCall = false;
      for (let j = 0; j < parts.length; j++) {
        const p = parts[j];
        if (p.functionCall) {
          if (!foundCall && !p.thoughtSignature) {
            debugLogger.warn(
              `[HistoryHardener] Missing thought signature on first function call in model turn. Injecting synthetic signature.`,
            );
            parts[j] = { ...p, thoughtSignature: SYNTHETIC_THOUGHT_SIGNATURE };
          }
          foundCall = true;
        }
      }

      // B. Pairing
      const callParts = parts.filter((p) => !!p.functionCall);
      if (callParts.length > 0) {
        const nextTurn = work[i + 1];
        const missing: Array<{ id: string; name: string }> = [];

        for (const call of callParts) {
          const id = call.functionCall!.id;
          const name = call.functionCall!.name || 'unknown';

          const hasResponse =
            nextTurn?.content.role === 'user' &&
            nextTurn.content.parts?.some(
              (p) =>
                p.functionResponse?.id === id &&
                p.functionResponse?.name === name,
            );

          if (!hasResponse) {
            debugLogger.log(
              `[HistoryHardener] Call id='${id}' (name='${name}') has no matching response in next turn.`,
            );
            missing.push({ id: id || '', name });
          }
        }

        if (missing.length > 0) {
          debugLogger.log(
            `[HistoryHardener] Detected ${missing.length} tool calls without responses. Injecting sentinel responses.`,
          );

          let targetUserTurn: HistoryTurn;
          if (nextTurn?.content.role === 'user') {
            targetUserTurn = nextTurn;
          } else {
            targetUserTurn = {
              id: deriveStableId([turn.id, 'sentinel_resp']),
              content: { role: 'user', parts: [] },
            };
            work.splice(i + 1, 0, targetUserTurn);
          }

          for (const m of missing) {
            targetUserTurn.content.parts = targetUserTurn.content.parts || [];
            targetUserTurn.content.parts.push({
              functionResponse: {
                name: m.name,
                id: m.id || undefined,
                response: {
                  error: sentinels.lostToolResponse,
                },
              },
            });
          }
        }
      }
    } else if (turn.content.role === 'user') {
      // C. Orphaned Responses
      // A user response MUST follow a model call.
      const prevTurn = result[result.length - 1];
      const parts = turn.content.parts || [];
      const validParts: Part[] = [];
      const orphanedResponses: Part[] = [];

      for (const p of parts) {
        if (p.functionResponse) {
          const id = p.functionResponse.id;
          const name = p.functionResponse.name;
          const hasCall =
            prevTurn?.content.role === 'model' &&
            prevTurn.content.parts?.some(
              (cp) =>
                cp.functionCall?.id === id && cp.functionCall?.name === name,
            );

          if (hasCall) {
            validParts.push(p);
          } else {
            debugLogger.log(
              `[HistoryHardener] Orphaned functionResponse id='${id}' (name='${name}'). Injecting synthetic functionCall.`,
            );
            orphanedResponses.push(p);
            validParts.push(p);
          }
        } else {
          validParts.push(p);
        }
      }

      if (orphanedResponses.length > 0) {
        let targetModelTurn: HistoryTurn;
        if (prevTurn?.content.role === 'model') {
          targetModelTurn = prevTurn;
        } else {
          targetModelTurn = {
            id: deriveStableId([turn.id, 'sentinel_call']),
            content: { role: 'model', parts: [] },
          };
          result.push(targetModelTurn);
        }

        for (const orph of orphanedResponses) {
          targetModelTurn.content.parts = targetModelTurn.content.parts || [];
          const hasExistingCall = targetModelTurn.content.parts.some(
            (p) => !!p.functionCall,
          );
          const callPart: Part = {
            functionCall: {
              name: orph.functionResponse!.name,
              id: orph.functionResponse!.id,
              args: {},
            },
          };
          if (!hasExistingCall) {
            callPart.thoughtSignature = SYNTHETIC_THOUGHT_SIGNATURE;
          }
          targetModelTurn.content.parts.push(callPart);
        }
      }

      turn.content.parts = validParts;
    }

    if (turn.content.parts && turn.content.parts.length > 0) {
      result.push(turn);
    }
  }

  return result;
}

/**
 * Hoists and re-orders tool responses within user turns to match preceding model turns.
 */
function refineToolResponses(history: HistoryTurn[]): HistoryTurn[] {
  for (let i = 1; i < history.length; i++) {
    const turn = history[i];
    const prev = history[i - 1];

    if (turn.content.role === 'user' && prev.content.role === 'model') {
      const callOrder =
        prev.content.parts
          ?.filter((p) => !!p.functionCall)
          .map((p) => p.functionCall!.id) || [];

      if (callOrder.length > 0) {
        const responseParts =
          turn.content.parts?.filter((p) => !!p.functionResponse) || [];
        const otherParts =
          turn.content.parts?.filter((p) => !p.functionResponse) || [];

        if (responseParts.length > 0) {
          // 1. Re-order: Sort responses to match the model's call order
          responseParts.sort((a, b) => {
            const idA = a.functionResponse!.id;
            const idB = b.functionResponse!.id;
            const idxA = callOrder.indexOf(idA);
            const idxB = callOrder.indexOf(idB);

            // If an ID isn't found in the preceding turn (should be rare after pairing),
            // move it to the end.
            if (idxA === -1) return 1;
            if (idxB === -1) return -1;
            return idxA - idxB;
          });

          // 2. Hoisting: Place all sorted responses BEFORE text or other parts
          turn.content.parts = [...responseParts, ...otherParts];
        }
      }
    }
  }
  return history;
}

/**
 * Final pass to ensure start/end roles and alternation are correct.
 */
function enforceRoleConstraints(
  history: HistoryTurn[],
  sentinels: Required<NonNullable<HardeningOptions['sentinels']>>,
): HistoryTurn[] {
  if (history.length === 0) return [];

  // Re-coalesce first to catch any empty turns or adjacent roles introduced by pairing
  const base = coalesce(history);
  if (base.length === 0) return [];

  const result: HistoryTurn[] = [...base];

  // 1. Ensure starts with user
  if (result[0].content.role === 'model') {
    debugLogger.log(
      '[HistoryHardener] Final history starts with model role. Prepending sentinel user turn.',
    );
    result.unshift({
      id: deriveStableId([result[0].id, 'sentinel_start']),
      content: {
        role: 'user',
        parts: [{ text: sentinels.continuation }],
      },
    });
  }

  // 2. Ensure ends with user
  if (result[result.length - 1].content.role === 'model') {
    debugLogger.log(
      '[HistoryHardener] Final history ends with model role. Appending sentinel user turn.',
    );
    result.push({
      id: deriveStableId([result[result.length - 1].id, 'sentinel_end']),
      content: {
        role: 'user',
        parts: [{ text: 'Please continue.' }],
      },
    });
  }

  // 3. Final Alternation Check (redundant if coalesce works, but safe)
  return coalesce(result);
}

/**
 * Deep-scrubs the history to remove any non-standard properties from Content and Part objects.
 * This ensures compatibility with strict APIs (like Vertex AI) that reject unknown fields.
 */
export function scrubHistory(history: HistoryTurn[]): HistoryTurn[] {
  const result: HistoryTurn[] = [];
  for (const turn of history) {
    const nonThoughtParts = (turn.content.parts ?? []).filter(
      (p) => p && !isInternalThought(p),
    );
    if (nonThoughtParts.length === 0) continue; // Skip turns that became empty

    const scrubbedParts = nonThoughtParts.map((p) => scrubPart(p));

    const lastIdx = result.length - 1;
    const last = result[lastIdx];
    if (last && last.content.role === turn.content.role) {
      // Coalesce inline with strict immutability
      result[lastIdx] = {
        id: last.id,
        content: {
          ...last.content,
          parts: [...(last.content.parts || []), ...scrubbedParts],
        },
      };
    } else {
      result.push({
        id: turn.id,
        content: {
          role: turn.content.role,
          parts: scrubbedParts,
        },
      });
    }
  }
  return result;
}

/**
 * Deep-scrubs an array of Content objects to remove non-standard properties.
 * Coalesces adjacent turns of the same role to preserve Gemini API alternation invariants.
 */
export function scrubContents(contents: Content[]): Content[] {
  const result: Content[] = [];
  for (const content of contents) {
    const nonThoughtParts = (content.parts ?? []).filter(
      (p) => p && !isInternalThought(p),
    );
    if (nonThoughtParts.length === 0) continue; // Skip turns that became empty after thought stripping

    const scrubbedParts = nonThoughtParts.map((p) => scrubPart(p));

    const lastIdx = result.length - 1;
    const last = result[lastIdx];
    if (last && last.role === content.role) {
      // Coalesce adjacent turns of the same role inline
      result[lastIdx] = {
        role: last.role,
        parts: [...(last.parts || []), ...scrubbedParts],
      };
    } else {
      result.push({
        role: content.role,
        parts: scrubbedParts,
      });
    }
  }
  return result;
}

interface ThoughtPart extends Part {
  thought?: boolean;
  thoughtSignature?: string;
}

function isThoughtPart(part: Part): part is ThoughtPart {
  return 'thoughtSignature' in part;
}

export function scrubPart(part: Part): Part {
  const scrubbed: Record<string, unknown> = {};

  if ('text' in part && typeof part.text === 'string') {
    scrubbed['text'] = part.text;
  }
  if ('inlineData' in part) {
    scrubbed['inlineData'] = part.inlineData;
  }
  if ('functionCall' in part && part.functionCall) {
    const scrubbedCall: Record<string, unknown> = {
      name: part.functionCall.name,
      args: part.functionCall.args,
    };
    if (part.functionCall.id) {
      scrubbedCall['id'] = part.functionCall.id;
    }
    scrubbed['functionCall'] = scrubbedCall;
  }
  if (isThoughtPart(part)) {
    scrubbed['thoughtSignature'] = part.thoughtSignature;
  }
  if ('functionResponse' in part && part.functionResponse) {
    const scrubbedResp: Record<string, unknown> = {
      name: part.functionResponse.name,
      response: part.functionResponse.response,
    };
    if (part.functionResponse.id) {
      scrubbedResp['id'] = part.functionResponse.id;
    }
    scrubbed['functionResponse'] = scrubbedResp;
  }
  if ('fileData' in part) {
    scrubbed['fileData'] = part.fileData;
  }
  if ('executableCode' in part) {
    scrubbed['executableCode'] = part.executableCode;
  }
  if ('codeExecutionResult' in part) {
    scrubbed['codeExecutionResult'] = part.codeExecutionResult;
  }

  // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
  return scrubbed as unknown as Part;
}
