import fs from 'node:fs';
import path from 'node:path';
import crypto from 'node:crypto';
import { getDb, nowIso, newId } from './db.js';
import { DATA_DIR } from './paths.js';
import { appendProjectChangelog, getProject, ensureProjectRootFolder } from './projects.js';
import { publishStateChanged } from './live-events.js';

export const DISK_DIR = path.join(DATA_DIR, 'disk');

function kindForName(name, mime = '') {
  const ext = path.extname(name || '').toLowerCase();
  if (['.md', '.markdown'].includes(ext)) return 'md';
  if (ext === '.pdf') return 'pdf';
  if (['.mp3', '.wav', '.m4a', '.ogg', '.webm'].includes(ext) || mime.startsWith('audio/')) return 'audio';
  if (['.png', '.jpg', '.jpeg', '.webp', '.gif'].includes(ext) || mime.startsWith('image/')) return 'image';
  if (['.txt', '.csv', '.json', '.yaml', '.yml'].includes(ext) || mime.startsWith('text/')) return 'text';
  return 'other';
}
function rowToFolder(r){ return r ? { ...r } : null; }
function rowToFile(r){ return r ? { ...r, local_only: r.local_only === 1 } : null; }

function emitDiskChanged(meta = {}) {
  try { publishStateChanged('disk', meta); } catch { /* live updates are observation-only */ }
}

export function listFolders(projectId) {
  const db = getDb();
  if (projectId) return db.prepare('SELECT * FROM disk_folders WHERE project_id = ? ORDER BY path ASC').all(projectId).map(rowToFolder);
  return db.prepare('SELECT * FROM disk_folders ORDER BY path ASC').all().map(rowToFolder);
}
export function getRootFolder(projectId) {
  const project = getProject(projectId);
  if (!project) throw new Error('project not found');
  ensureProjectRootFolder(project.id, project.slug);
  return rowToFolder(getDb().prepare('SELECT * FROM disk_folders WHERE project_id = ? AND parent_id IS NULL LIMIT 1').get(project.id));
}
export function createFolder(projectId, parentId, name) {
  if (typeof projectId === 'object' && projectId) {
    const input = projectId;
    projectId = input.project_id || input.projectId;
    parentId = input.parent_id || input.parentId || null;
    name = input.name;
  }
  const project = getProject(projectId);
  if (!project) throw new Error('project not found');
  const clean = String(name || '').trim();
  if (!clean) throw new Error('name is required');
  const db = getDb();
  const parent = parentId ? db.prepare('SELECT * FROM disk_folders WHERE id = ?').get(parentId) : getRootFolder(project.id);
  if (!parent) throw new Error('parent folder not found');
  const existing = db.prepare('SELECT * FROM disk_folders WHERE project_id = ? AND parent_id = ? AND name = ? LIMIT 1')
    .get(project.id, parent.id, clean);
  if (existing) {
    const err = new Error(`Folder "${clean}" already exists.`);
    err.code = 'FOLDER_EXISTS';
    throw err;
  }
  const folderPath = `${parent.path.replace(/\/$/, '')}/${clean}/`;
  const now = nowIso();
  const id = newId('df');
  try {
    db.prepare(`INSERT INTO disk_folders (id, project_id, parent_id, name, path, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?)`)
      .run(id, project.id, parent.id, clean, folderPath, now, now);
  } catch (e) {
    if (String(e?.message || '').includes('UNIQUE constraint failed')) {
      const err = new Error(`Folder "${clean}" already exists.`);
      err.code = 'FOLDER_EXISTS';
      throw err;
    }
    throw e;
  }
  const osDir = path.join(DISK_DIR, folderPath.replace(/^\/+|\/+$/g, ''));
  fs.mkdirSync(osDir, { recursive: true });
  const folder = rowToFolder(db.prepare('SELECT * FROM disk_folders WHERE id = ?').get(id));
  emitDiskChanged({ project_id: project.id, folder_id: folder?.id || id, action: 'folder.create' });
  return folder;
}
export function listFiles(opts = {}) {
  const db = getDb();
  const where = []; const params = [];
  if (opts.project_id) { where.push('project_id = ?'); params.push(opts.project_id); }
  if (opts.folder_id) { where.push('folder_id = ?'); params.push(opts.folder_id); }
  const sql = `SELECT * FROM disk_files ${where.length ? 'WHERE '+where.join(' AND ') : ''} ORDER BY updated_at DESC`;
  return db.prepare(sql).all(...params).map(rowToFile);
}

// Batched count of disk files per project. One query instead of one-per-project.
// Returns a plain object keyed by project_id (string).
export function countFilesByProject() {
  const rows = getDb().prepare(
    `SELECT project_id, COUNT(*) AS count
       FROM disk_files
      WHERE project_id IS NOT NULL
      GROUP BY project_id`
  ).all();
  const map = Object.create(null);
  for (const row of rows) map[row.project_id] = row.count;
  return map;
}
export function getDiskFile(id) { return rowToFile(getDb().prepare('SELECT * FROM disk_files WHERE id = ?').get(id)); }
export function uploadFile(projectId, folderId, file, opts = {}) {
  if (typeof projectId === 'object' && projectId) {
    const input = projectId;
    projectId = input.project_id || input.projectId;
    folderId = input.folder_id || input.folderId || null;
    file = { name: input.name, content: input.content || input.buffer || '', type: input.mime_type || input.type || '' };
    opts = { ...input, local_only: input.local_only };
  }
  const project = getProject(projectId);
  if (!project) throw new Error('project not found');
  const folder = folderId ? getDb().prepare('SELECT * FROM disk_folders WHERE id = ?').get(folderId) : getRootFolder(project.id);
  if (!folder) throw new Error('folder not found');
  const buffer = Buffer.isBuffer(file?.buffer) ? file.buffer : Buffer.from(file?.content || '');
  const name = String(file?.name || opts.name || 'upload.bin').replace(/[\\/]+/g, '-');
  const sha256 = crypto.createHash('sha256').update(buffer).digest('hex');
  const dir = path.join(DISK_DIR, folder.path.replace(/^\/+|\/+$/g, ''));
  fs.mkdirSync(dir, { recursive: true });
  const storagePath = path.join(dir, `${sha256.slice(0, 12)}-${name}`);
  if (!fs.existsSync(storagePath)) fs.writeFileSync(storagePath, buffer);
  const now = nowIso(); const id = newId('file');
  getDb().prepare(`INSERT INTO disk_files (id, project_id, folder_id, name, kind, mime_type, size_bytes, sha256, storage_path, local_only, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
    .run(id, project.id, folder.id, name, kindForName(name, file?.type || opts.mime_type || ''), file?.type || opts.mime_type || null, buffer.length, sha256, storagePath, opts.local_only ? 1 : 0, now, now);
  const uploaded = getDiskFile(id);
  try {
    appendProjectChangelog(project.id, {
      action: 'project.file.uploaded',
      title: `Uploaded ${name}`,
      summary: `Attached ${name} to the project folder`,
      status: 'complete',
      source: 'disk',
      details: { file_id: id, folder_id: folder.id, kind: uploaded.kind, size_bytes: uploaded.size_bytes, local_only: uploaded.local_only },
    });
  } catch {}
  emitDiskChanged({ project_id: project.id, folder_id: folder.id, file_id: id, action: 'file.upload' });
  return uploaded;
}
export function updateFile(id, fields = {}) {
  const allowed = ['name','folder_id','local_only'];
  const entries = Object.entries(fields).filter(([k,v]) => allowed.includes(k) && v !== undefined);
  if (!entries.length) return getDiskFile(id);
  const sets = entries.map(([k]) => `${k} = ?`).concat('updated_at = ?').join(', ');
  const vals = entries.map(([k,v]) => k === 'local_only' ? (v ? 1 : 0) : v).concat(nowIso(), id);
  getDb().prepare(`UPDATE disk_files SET ${sets} WHERE id = ?`).run(...vals);
  const file = getDiskFile(id);
  emitDiskChanged({ project_id: file?.project_id || null, folder_id: file?.folder_id || null, file_id: id, action: 'file.update' });
  return file;
}
export function deleteFile(id) {
  const file = getDiskFile(id);
  if (!file) return false;
  getDb().prepare('DELETE FROM disk_files WHERE id = ?').run(id);
  try { fs.unlinkSync(file.storage_path); } catch {}
  emitDiskChanged({ project_id: file.project_id, folder_id: file.folder_id || null, file_id: id, action: 'file.delete' });
  return true;
}
export function readFileForEngine(fileId, engine) {
  if (!engine) throw new Error('engine parameter required');
  const file = getDiskFile(fileId);
  if (!file) throw new Error('file not found');
  if (file.local_only && !['ollama', 'local_whisper'].includes(String(engine))) {
    return { ...file, content: '<redacted: local_only>', redacted: true };
  }
  return { ...file, content: fs.readFileSync(file.storage_path, 'utf8'), redacted: false };
}
export function streamPathForEngine(fileId, engine) {
  const result = readFileForEngine(fileId, engine);
  if (result.redacted) { const err = new Error('local_only: engine not permitted'); err.code = 'LOCAL_ONLY_DENIED'; throw err; }
  return result.storage_path;
}

export const writeDiskFile = uploadFile;
