import fs from 'node:fs';
import path from 'node:path';
import crypto from 'node:crypto';
import { DATA_DIR } from './paths.js';

// Chat attachment storage. Files uploaded from the main-agent composer land
// under DATA_DIR/uploads as two files per upload: `<id>` (raw bytes) and
// `<id>.json` (sidecar metadata). The client only ever holds the opaque `id`;
// the server resolves bytes by id, so a malicious client cannot point the read
// side at an arbitrary path (id is validated against a strict hex pattern).

export const UPLOADS_DIR = path.join(DATA_DIR, 'uploads');

// 20 MB hard cap per file. Anthropic's own limits are lower per modality
// (images ~5 MB, PDF ~32 MB) but 20 MB is a sane upper bound for a local tool.
export const MAX_UPLOAD_BYTES = 20 * 1024 * 1024;

// Attachments per single message.
export const MAX_ATTACHMENTS = 8;

const IMAGE_MIMES = new Set(['image/png', 'image/jpeg', 'image/gif', 'image/webp']);
const PDF_MIME = 'application/pdf';

// Text-like extensions we inline directly into the prompt. MIME from the
// browser is unreliable for code files (often application/octet-stream), so we
// classify by extension as well.
const TEXT_EXTS = new Set([
  '.txt', '.md', '.markdown', '.csv', '.tsv', '.json', '.jsonl', '.log', '.yaml', '.yml',
  '.xml', '.html', '.htm', '.css', '.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs',
  '.py', '.rb', '.go', '.rs', '.java', '.c', '.h', '.cpp', '.hpp', '.cs', '.php',
  '.sh', '.bash', '.zsh', '.sql', '.toml', '.ini', '.env', '.conf', '.tex', '.rst',
]);

const ID_RE = /^[0-9a-f]{32}$/;

/**
 * Classify an attachment so the orchestrator knows how to feed it to the model.
 * - 'image' / 'pdf' → native Anthropic content blocks (vision / documents).
 * - 'text' → inlined into the prompt as a fenced block (works on every engine).
 * - 'other' → referenced by name only (binary we can't interpret).
 */
export function classifyAttachment(mime, name) {
  const m = (mime || '').toLowerCase();
  const ext = path.extname(name || '').toLowerCase();
  if (IMAGE_MIMES.has(m)) return 'image';
  if (m === PDF_MIME || ext === '.pdf') return 'pdf';
  if (m.startsWith('text/') || m === 'application/json' || TEXT_EXTS.has(ext)) return 'text';
  return 'other';
}

export function isAllowedAttachment(mime, name) {
  return classifyAttachment(mime, name) !== 'other';
}

function sanitizeName(name) {
  const base = path.basename(String(name || 'file')).replace(/[^\w.\- ]+/g, '_').trim();
  return base.slice(0, 160) || 'file';
}

function ensureDir() {
  fs.mkdirSync(UPLOADS_DIR, { recursive: true });
}

/**
 * Persist an uploaded file. Returns the metadata the client echoes back on the
 * next chat send. Never returns a filesystem path.
 */
export function saveUpload({ buffer, name, mime }) {
  if (!Buffer.isBuffer(buffer)) throw new Error('buffer required');
  if (buffer.length === 0) throw new Error('empty file');
  if (buffer.length > MAX_UPLOAD_BYTES) throw new Error('file too large');
  const cleanName = sanitizeName(name);
  if (!isAllowedAttachment(mime, cleanName)) throw new Error('unsupported file type');
  ensureDir();
  const id = crypto.randomBytes(16).toString('hex');
  const kind = classifyAttachment(mime, cleanName);
  const meta = { id, name: cleanName, mime: String(mime || 'application/octet-stream'), size: buffer.length, kind };
  fs.writeFileSync(path.join(UPLOADS_DIR, id), buffer);
  fs.writeFileSync(path.join(UPLOADS_DIR, `${id}.json`), JSON.stringify(meta));
  return meta;
}

/**
 * Resolve an upload id to its bytes + metadata. Returns null if the id is
 * malformed or missing. Strict id validation prevents path traversal.
 */
export function readUpload(id) {
  if (typeof id !== 'string' || !ID_RE.test(id)) return null;
  const dataPath = path.join(UPLOADS_DIR, id);
  const metaPath = path.join(UPLOADS_DIR, `${id}.json`);
  try {
    const meta = JSON.parse(fs.readFileSync(metaPath, 'utf8'));
    const buffer = fs.readFileSync(dataPath);
    return { ...meta, buffer };
  } catch {
    return null;
  }
}

/**
 * Resolve a list of client-supplied attachment refs (each at least `{ id }`)
 * into loaded attachments with bytes. Silently drops ids that don't resolve.
 */
export function loadAttachments(refs) {
  if (!Array.isArray(refs)) return [];
  const out = [];
  for (const ref of refs.slice(0, MAX_ATTACHMENTS)) {
    const id = ref && typeof ref === 'object' ? ref.id : ref;
    const loaded = readUpload(id);
    if (loaded) out.push(loaded);
  }
  return out;
}

const MAX_INLINE_TEXT = 60000; // chars of a text file inlined into the prompt

/**
 * Build Anthropic content blocks for a set of loaded attachments. Pair with the
 * user's text in a single content array. Image/PDF become native blocks; text
 * files are inlined; anything else is referenced by name.
 */
export function attachmentsToContentBlocks(loaded) {
  const blocks = [];
  for (const a of loaded) {
    if (a.kind === 'image') {
      blocks.push({
        type: 'image',
        source: { type: 'base64', media_type: a.mime, data: a.buffer.toString('base64') },
      });
    } else if (a.kind === 'pdf') {
      blocks.push({
        type: 'document',
        source: { type: 'base64', media_type: 'application/pdf', data: a.buffer.toString('base64') },
      });
    } else if (a.kind === 'text') {
      const text = a.buffer.toString('utf8').slice(0, MAX_INLINE_TEXT);
      const truncated = a.buffer.length > MAX_INLINE_TEXT ? '\n…(truncated)' : '';
      blocks.push({ type: 'text', text: `Attached file "${a.name}":\n\n\`\`\`\n${text}${truncated}\n\`\`\`` });
    } else {
      blocks.push({ type: 'text', text: `Attached file "${a.name}" (${a.mime}, ${a.size} bytes) — binary, contents not shown.` });
    }
  }
  return blocks;
}

/**
 * Plain-text representation of attachments for engines without content-block
 * support (Ollama, Claude Code / Codex CLI subprocesses). Text files inline;
 * images/PDF noted with a hint to switch engines for analysis.
 */
export function attachmentsToPromptText(loaded) {
  if (!loaded.length) return '';
  const parts = ['', '--- Attachments ---'];
  for (const a of loaded) {
    if (a.kind === 'text') {
      const text = a.buffer.toString('utf8').slice(0, MAX_INLINE_TEXT);
      const truncated = a.buffer.length > MAX_INLINE_TEXT ? '\n…(truncated)' : '';
      parts.push(`\nFile "${a.name}":\n\`\`\`\n${text}${truncated}\n\`\`\``);
    } else if (a.kind === 'image') {
      parts.push(`\n[Image attached: "${a.name}". This engine cannot view images — switch to an Anthropic model to analyze it.]`);
    } else if (a.kind === 'pdf') {
      parts.push(`\n[PDF attached: "${a.name}". This engine cannot read PDFs — switch to an Anthropic model to analyze it.]`);
    } else {
      parts.push(`\n[Attached "${a.name}" (${a.mime}) — binary, not shown.]`);
    }
  }
  return parts.join('\n');
}
