'use strict';

const crypto = require('node:crypto');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');

const DEFAULT_MAX_INLINE_BYTES = 50 * 1024;
const DEFAULT_PREVIEW_BYTES = 8 * 1024;

class ArtifactStore {
  constructor({
    artifactsDir = defaultArtifactsDir(),
    now = () => new Date().toISOString(),
    maxInlineBytes = DEFAULT_MAX_INLINE_BYTES,
    previewBytes = DEFAULT_PREVIEW_BYTES,
  } = {}) {
    this.artifactsDir = artifactsDir;
    this.now = now;
    this.maxInlineBytes = maxInlineBytes;
    this.previewBytes = previewBytes;
  }

  routeToolOutput(result, { sessionId = '', toolCallId = '', toolName = '', metadata = {} } = {}) {
    const serialized = serializeResult(result);
    const bytes = Buffer.byteLength(serialized, 'utf8');
    if (bytes <= this.maxInlineBytes) return { result, artifact: null };

    const artifact = this.storeToolOutput({
      sessionId,
      toolCallId,
      toolName,
      content: serialized,
      mimeType: 'application/json',
      metadata: { ...metadata, originalBytes: bytes },
    });
    return {
      result: attachArtifactSummary(result, artifact, serialized, this.previewBytes),
      artifact,
    };
  }

  storeToolOutput({ sessionId = '', toolCallId = '', toolName = '', content = '', mimeType = 'text/plain', metadata = {} } = {}) {
    const text = String(content ?? '');
    const bytes = Buffer.byteLength(text, 'utf8');
    const sha256 = crypto.createHash('sha256').update(text).digest('hex');
    const artifactId = `art_${sha256.slice(0, 16)}`;
    const dir = path.join(this.artifactsDir, safeName(sessionId || 'session'));
    fs.mkdirSync(dir, { recursive: true });
    const ext = mimeType.includes('json') ? 'json' : 'txt';
    const filePath = path.join(dir, `${safeName(toolCallId || toolName || artifactId)}-${sha256.slice(0, 12)}.${ext}`);
    fs.writeFileSync(filePath, text, 'utf8');
    return {
      artifactId,
      kind: 'tool_output',
      sessionId: String(sessionId || ''),
      toolCallId: String(toolCallId || ''),
      toolName: String(toolName || ''),
      path: filePath,
      bytes,
      sha256,
      mimeType,
      createdAt: this.now(),
      metadata: safeJsonValue(metadata),
    };
  }

  storeArtifact({
    sessionId = '',
    toolCallId = '',
    toolName = '',
    kind = 'artifact',
    sourcePath = '',
    path: artifactPath = '',
    content = null,
    mimeType = 'application/octet-stream',
    bytes = null,
    sha256 = '',
    metadata = {},
  } = {}) {
    const inputPath = sourcePath || artifactPath || '';
    let buffer = null;
    let ext = extensionForArtifact(inputPath, mimeType);
    if (content !== null && content !== undefined) {
      buffer = Buffer.isBuffer(content) ? content : Buffer.from(String(content));
    } else if (inputPath && fs.existsSync(inputPath) && fs.statSync(inputPath).isFile()) {
      buffer = fs.readFileSync(inputPath);
      if (!mimeType || mimeType === 'application/octet-stream') mimeType = mimeTypeForPath(inputPath, mimeType);
    } else if (Number.isFinite(Number(bytes)) && sha256) {
      return {
        artifactId: `art_${String(sha256).slice(0, 16)}`,
        kind: String(kind || 'artifact'),
        sessionId: String(sessionId || ''),
        toolCallId: String(toolCallId || ''),
        toolName: String(toolName || ''),
        path: String(inputPath || ''),
        bytes: Number(bytes),
        sha256: String(sha256),
        mimeType,
        createdAt: this.now(),
        metadata: safeJsonValue(metadata),
      };
    } else {
      throw new Error('storeArtifact requires content, an existing sourcePath/path, or bytes+sha256 metadata');
    }

    const realBytes = buffer.length;
    const realSha = sha256 || crypto.createHash('sha256').update(buffer).digest('hex');
    const artifactId = `art_${realSha.slice(0, 16)}`;
    const dir = path.join(this.artifactsDir, safeName(sessionId || 'session'));
    fs.mkdirSync(dir, { recursive: true });
    const filePath = path.join(dir, `${safeName(toolCallId || toolName || kind || artifactId)}-${realSha.slice(0, 12)}.${ext}`);
    if (inputPath && path.resolve(inputPath) === path.resolve(filePath)) {
      // Already in the artifact store.
    } else {
      fs.writeFileSync(filePath, buffer);
    }
    return {
      artifactId,
      kind: String(kind || 'artifact'),
      sessionId: String(sessionId || ''),
      toolCallId: String(toolCallId || ''),
      toolName: String(toolName || ''),
      path: filePath,
      originalPath: inputPath ? String(inputPath) : '',
      bytes: realBytes,
      sha256: realSha,
      mimeType,
      createdAt: this.now(),
      metadata: safeJsonValue(metadata),
    };
  }
}

function defaultArtifactsDir(env = process.env) {
  const dataDir = env.WALL_E_DATA_DIR || env.WALLE_DATA_DIR || path.join(os.homedir(), '.walle', 'data');
  return path.join(dataDir, 'artifacts');
}

function serializeResult(result) {
  if (typeof result === 'string') return result;
  try {
    return JSON.stringify(result ?? null, null, 2);
  } catch {
    return String(result ?? '');
  }
}

function attachArtifactSummary(result, artifact, serialized, previewBytes) {
  const preview = truncateUtf8(serialized, previewBytes);
  const publicRef = publicArtifactRef(artifact);
  if (result && typeof result === 'object' && !Array.isArray(result)) {
    const copy = safeJsonValue(result);
    for (const [key, value] of Object.entries(copy)) {
      if (typeof value === 'string' && Buffer.byteLength(value, 'utf8') > previewBytes) {
        copy[key] = truncateUtf8(value, previewBytes);
        copy[`${key}_truncated`] = true;
      }
    }
    return {
      ...copy,
      truncated: true,
      preview,
      artifact: publicRef,
    };
  }
  return {
    output: preview,
    truncated: true,
    artifact: publicRef,
  };
}

function publicArtifactRef(artifact) {
  return {
    artifactId: artifact.artifactId,
    kind: artifact.kind,
    sessionId: artifact.sessionId,
    path: artifact.path,
    originalPath: artifact.originalPath,
    bytes: artifact.bytes,
    sha256: artifact.sha256,
    mimeType: artifact.mimeType,
    toolCallId: artifact.toolCallId,
    toolName: artifact.toolName,
    createdAt: artifact.createdAt,
    metadata: safeJsonValue(artifact.metadata || {}),
  };
}

function truncateUtf8(text, maxBytes) {
  const value = String(text || '');
  if (Buffer.byteLength(value, 'utf8') <= maxBytes) return value;
  let bytes = 0;
  let out = '';
  for (const char of value) {
    const next = Buffer.byteLength(char, 'utf8');
    if (bytes + next > maxBytes) break;
    out += char;
    bytes += next;
  }
  return `${out}\n[tool output truncated; full content stored as artifact]`;
}

function safeName(value) {
  return String(value || 'artifact')
    .replace(/[^A-Za-z0-9_.:-]+/g, '_')
    .replace(/^_+|_+$/g, '')
    .slice(0, 80) || 'artifact';
}

function extensionForArtifact(filePath, mimeType) {
  const ext = path.extname(String(filePath || '')).replace(/^\./, '').toLowerCase();
  if (ext) return safeName(ext).slice(0, 12) || 'bin';
  if (/pdf/i.test(mimeType)) return 'pdf';
  if (/png/i.test(mimeType)) return 'png';
  if (/jpe?g/i.test(mimeType)) return 'jpg';
  if (/html/i.test(mimeType)) return 'html';
  if (/json/i.test(mimeType)) return 'json';
  if (/text/i.test(mimeType)) return 'txt';
  return 'bin';
}

function mimeTypeForPath(filePath, fallback) {
  const ext = path.extname(String(filePath || '')).toLowerCase();
  if (ext === '.pdf') return 'application/pdf';
  if (ext === '.png') return 'image/png';
  if (ext === '.jpg' || ext === '.jpeg') return 'image/jpeg';
  if (ext === '.html' || ext === '.htm') return 'text/html';
  if (ext === '.json') return 'application/json';
  if (ext === '.txt' || ext === '.md') return 'text/plain';
  return fallback || 'application/octet-stream';
}

function safeJsonValue(value) {
  try {
    return JSON.parse(JSON.stringify(value));
  } catch {
    return String(value);
  }
}

module.exports = {
  ArtifactStore,
  defaultArtifactsDir,
  publicArtifactRef,
  serializeResult,
  DEFAULT_MAX_INLINE_BYTES,
  DEFAULT_PREVIEW_BYTES,
};
