import { mkdirSync, writeFileSync, appendFileSync } from 'node:fs';
import path from 'node:path';
import { randomBytes } from 'node:crypto';
import { logger } from '../logger.js';
import { redactSecrets } from './redact.js';

export interface SaveMcpRawInput {
  workspacePath: string;
  serverId: string;
  toolName: string;
  args: Record<string, unknown>;
  content: unknown[];
  isError: boolean;
  output: string;
  savedPaths: string[];
}

function sanitizeSlug(raw: string, fallback: string): string {
  const cleaned = raw.toLowerCase().replace(/[^a-z0-9_-]/g, '-').slice(0, 64);
  return cleaned.length > 0 ? cleaned : fallback;
}

function stampForFilename(): string {
  return new Date().toISOString().replace(/[:.]/g, '-');
}

interface ContentBlockLike {
  type?: unknown;
  data?: unknown;
  resource?: { blob?: unknown; [k: string]: unknown };
  [k: string]: unknown;
}

function stripBase64(content: unknown[]): unknown[] {
  return content.map((block) => {
    if (!block || typeof block !== 'object') return block;
    const b = block as ContentBlockLike;
    const clone: ContentBlockLike = { ...b };
    if (typeof b.data === 'string' && b.data.length > 0) {
      clone.data = `<base64 omitted (${b.data.length} chars) — see savedBinaries>`;
    }
    if (b.resource && typeof b.resource === 'object') {
      const r = b.resource as { blob?: unknown };
      if (typeof r.blob === 'string' && r.blob.length > 0) {
        clone.resource = {
          ...b.resource,
          blob: `<base64 omitted (${r.blob.length} chars) — see savedBinaries>`,
        };
      }
    }
    return clone;
  });
}

/**
 * Persist a raw MCP tool-call response to {workspace}/logs/mcp/{serverId}/{toolName}-{stamp}-{hash}.json
 * and append a one-line summary to {workspace}/logs/mcp-history.jsonl.
 *
 * Best-effort: never throws — failures are logged at warn level and the caller continues.
 * Base64 payloads inside image/resource blocks are replaced with a reference string
 * because the same bytes are already saved separately under output/mcp/ by binary-saver.
 */
export function saveMcpRaw(input: SaveMcpRawInput): void {
  try {
    const serverSlug = sanitizeSlug(input.serverId, 'unknown-server');
    const toolSlug = sanitizeSlug(input.toolName, 'unknown-tool');
    const stamp = stampForFilename();
    const hash = randomBytes(3).toString('hex');
    const filename = `${toolSlug}-${stamp}-${hash}.json`;

    const relDir = path.posix.join('logs', 'mcp', serverSlug);
    const absDir = path.join(input.workspacePath, relDir);
    const absPath = path.join(absDir, filename);

    // Path traversal defence: resolved absPath must stay inside workspace.
    const resolvedWorkspace = path.resolve(input.workspacePath);
    const resolvedAbs = path.resolve(absPath);
    if (!resolvedAbs.startsWith(resolvedWorkspace + path.sep)) {
      logger.warn(`[mcp:raw] resolved path escapes workspace; skip save`);
      return;
    }

    mkdirSync(absDir, { recursive: true });

    const payload = {
      timestamp: new Date().toISOString(),
      serverId: input.serverId,
      toolName: input.toolName,
      arguments: redactSecrets(input.args),
      isError: input.isError,
      content: stripBase64(input.content),
      output: input.output,
      savedBinaries: input.savedPaths,
    };
    const json = JSON.stringify(payload, null, 2);
    writeFileSync(absPath, json, 'utf-8');

    const relFile = path.posix.join(relDir, filename);
    const historyEntry = {
      timestamp: payload.timestamp,
      serverId: input.serverId,
      toolName: input.toolName,
      filename: relFile,
      isError: input.isError,
      bytes: Buffer.byteLength(json, 'utf-8'),
    };
    appendFileSync(
      path.join(input.workspacePath, 'logs', 'mcp-history.jsonl'),
      JSON.stringify(historyEntry) + '\n',
      'utf-8',
    );
  } catch (err) {
    logger.warn(`[mcp:raw] failed to save raw mcp data: ${(err as Error).message}`);
  }
}
