import fs from 'node:fs';
import path from 'node:path';
import { getDb, nowIso, newId } from './db.js';
import { WORKSPACE, DATA_DIR, resolveStoredPath } from './paths.js';

const PROJECT_BLOB_ROOT = path.join(DATA_DIR, 'disk');
const DEFAULT_SEED_MARKER = path.join(DATA_DIR, '.default-project-seeded');

export const DEFAULT_PROJECT_ID = 'personal';
const DEFAULT_PROJECT_NAME = 'Personal';
const DEFAULT_PROJECT_DESC = 'Your personal project — keep agents, files, memory, and integrations scoped to you.';
const DEFAULT_PROJECT_COLOR = '#c2452a';

const PROJECT_CHILD_TABLES = [
  'agents',
  'agent_template_installations',
  'agent_memory',
  'disk_files',
  'disk_folders',
  'missions',
  'transcripts',
  'artifacts',
  'heartbeat_tasks',
  'approval_requests',
  'audit_logs',
  'activity_log',
  'tool_egress',
  'secrets',
];

export function slugifyProject(input) {
  return String(input || 'project').toLowerCase().trim()
    .replace(/[^a-z0-9]+/g, '-')
    .replace(/^-+|-+$/g, '')
    .slice(0, 64) || 'project';
}

function rowToProject(row) {
  return row ? { ...row } : null;
}

function nextSlug(db, baseSlug) {
  let slug = slugifyProject(baseSlug);
  let n = 2;
  while (db.prepare('SELECT 1 FROM projects WHERE slug = ?').get(slug)) {
    slug = `${slugifyProject(baseSlug)}-${n++}`;
  }
  return slug;
}

export function defaultProjectFolderPath(slug) {
  return path.join(WORKSPACE, 'projects', slug);
}

export function resolveProjectFolderPath(input, slug) {
  const raw = String(input || '').trim();
  if (!raw) return defaultProjectFolderPath(slug);
  const expanded = raw.startsWith('~')
    ? path.join(process.env.HOME || '', raw.slice(1))
    : raw;
  if (!path.isAbsolute(expanded)) {
    const err = new Error('folder_path must be an absolute path (or start with ~)');
    err.code = 'PROJECT_FOLDER_NOT_ABSOLUTE';
    throw err;
  }
  return path.normalize(expanded);
}

function ensureFolderOnDisk(folderPath) {
  try {
    fs.mkdirSync(folderPath, { recursive: true });
  } catch (err) {
    const wrapped = new Error(`failed to create project folder: ${err.message}`);
    wrapped.code = 'PROJECT_FOLDER_MKDIR_FAILED';
    wrapped.cause = err;
    throw wrapped;
  }
}


function safeJson(value, fallback = {}) {
  if (!value) return fallback;
  try { return JSON.parse(value); } catch { return fallback; }
}

function capText(value, max = 900) {
  const text = String(value || '').replace(/\s+/g, ' ').trim();
  return text.length > max ? `${text.slice(0, max).trim()}...` : text;
}

const PREVIEW_FILE_KINDS = new Set(['md', 'markdown', 'text', 'txt', 'html', 'htm']);
const MAX_PREVIEW_BYTES = 512 * 1024;

function readFileBody(file, project) {
  if (!file || !project) return null;
  if (!PREVIEW_FILE_KINDS.has(String(file.kind || '').toLowerCase())) return null;
  const projectDiskRoot = path.join(PROJECT_BLOB_ROOT, project.slug);
  if (!isInsidePath(file.storage_path, projectDiskRoot)) return null;
  try {
    const stat = fs.statSync(file.storage_path);
    if (stat.size > MAX_PREVIEW_BYTES) {
      const fd = fs.openSync(file.storage_path, 'r');
      try {
        const buf = Buffer.alloc(MAX_PREVIEW_BYTES);
        fs.readSync(fd, buf, 0, MAX_PREVIEW_BYTES, 0);
        return { body: buf.toString('utf8'), truncated: true };
      } finally {
        fs.closeSync(fd);
      }
    }
    return { body: fs.readFileSync(file.storage_path, 'utf8'), truncated: false };
  } catch {
    return null;
  }
}

function isInsidePath(candidate, root) {
  try {
    const resolvedCandidate = fs.realpathSync(candidate || '');
    const resolvedRoot = fs.realpathSync(root || '');
    return resolvedCandidate === resolvedRoot || resolvedCandidate.startsWith(`${resolvedRoot}${path.sep}`);
  } catch {
    return false;
  }
}

function filePreview(file, project) {
  if (!file || !project) return null;
  if (!['md', 'text'].includes(file.kind)) return null;
  const projectDiskRoot = path.join(PROJECT_BLOB_ROOT, project.slug);
  if (!isInsidePath(file.storage_path, projectDiskRoot)) return null;
  try {
    const raw = fs.readFileSync(file.storage_path, 'utf8');
    return capText(raw, 1200);
  } catch {
    return null;
  }
}

export function appendProjectChangelog(projectId, input = {}) {
  if (!projectId) throw new Error('project_id is required');
  const db = getDb();
  const id = newId('activity');
  const now = input.created_at || nowIso();
  const explicitFolderId = input.folder_id !== undefined
    ? input.folder_id
    : (input.details && input.details.folder_id !== undefined ? input.details.folder_id : null);
  const details = {
    title: input.title || input.summary || 'Project update',
    summary: input.summary || input.title || 'Project update',
    status: input.status || null,
    source: input.source || 'system',
    update_kind: input.update_kind || null,
    ...input.details,
    folder_id: explicitFolderId,
  };
  db.prepare(`
    INSERT INTO activity_log
      (id, actor_type, actor_id, agent_id, run_id, entity_type, entity_id, action, details_json, created_at, project_id)
    VALUES (?, ?, ?, ?, ?, 'project', ?, ?, ?, ?, ?)
  `).run(
    id,
    input.actor_type || 'system',
    input.actor_id || null,
    input.agent_id || null,
    input.run_id || null,
    projectId,
    input.action || 'project.changelog',
    JSON.stringify(details),
    now,
    projectId
  );
  return db.prepare('SELECT seq, * FROM activity_log WHERE id = ?').get(id);
}

export function listProjectChangelog(projectId, opts = {}) {
  if (!projectId) return [];
  // Legacy: numeric arg = limit.
  const options = typeof opts === 'number' ? { limit: opts } : (opts || {});
  const limit = Math.max(1, Math.min(500, Number(options.limit) || 200));
  const db = getDb();
  const rows = db.prepare(`
    SELECT seq, id, actor_type, actor_id, agent_id, run_id, entity_type, entity_id,
           action, details_json, created_at, project_id
    FROM activity_log
    WHERE project_id = ? OR (entity_type = 'project' AND entity_id = ?)
    ORDER BY seq DESC
    LIMIT ?
  `).all(projectId, projectId, limit).map((row) => ({
    ...row,
    details: safeJson(row.details_json),
  }));
  return rows.filter((row) => {
    if (options.folderId === undefined) return true;
    const fid = row.details?.folder_id ?? null;
    if (options.folderId === null) return fid === null;
    return fid === options.folderId;
  }).filter((row) => {
    if (!options.updateKind) return true;
    return row.details?.update_kind === options.updateKind;
  });
}

// Group changelog rows by folder_id for the per-folder pinned views.
// Root entries (no folder_id) land under the 'root' key.
export function groupChangelogByFolder(entries = []) {
  const buckets = { root: [] };
  for (const entry of entries) {
    const fid = entry.details?.folder_id ?? null;
    const key = fid || 'root';
    if (!buckets[key]) buckets[key] = [];
    buckets[key].push(entry);
  }
  return buckets;
}

export function getProjectWorkspaceSummary(projectId, opts = {}) {
  const project = getProject(projectId);
  if (!project) return null;
  const db = getDb();
  const limit = Math.max(1, Math.min(200, Number(opts.limit) || 8));
  const files = db.prepare('SELECT * FROM disk_files WHERE project_id = ? ORDER BY updated_at DESC LIMIT ?').all(project.id, limit)
    .map((file) => {
      const fileKind = String(file.kind || '').toLowerCase();
      const body = readFileBody(file, project);
      const isHtml = fileKind === 'html' || fileKind === 'htm';
      const isMarkdown = fileKind === 'md' || fileKind === 'markdown';
      const renderKind = isHtml ? 'html' : (isMarkdown ? 'markdown' : (body ? 'text' : 'binary'));
      return {
        ...file,
        local_only: file.local_only === 1,
        preview: body ? capText(body.body, 1200) : null,
        body: body && !file.local_only ? body.body : null,
        body_truncated: body?.truncated || false,
        render_kind: renderKind,
        source_type: 'file',
      };
    });
  const folders = db.prepare('SELECT * FROM disk_folders WHERE project_id = ? ORDER BY path ASC').all(project.id).map((f) => ({ ...f }));
  const artifactRows = db.prepare(`
    SELECT * FROM artifacts
    WHERE project_id = ?
    ORDER BY created_at DESC
    LIMIT ?
  `).all(project.id, limit);
  // Filter rows whose on-disk file no longer exists ("ghost" artifacts).
  // The DB row is left intact; run scripts/clean-ghost-artifacts.mjs to purge.
  const liveArtifactRows = artifactRows.filter((row) => {
    if (!row || !row.path) return false;
    try { return fs.existsSync(resolveStoredPath(row.path)); } catch { return false; }
  });
  const artifacts = liveArtifactRows.map((artifact) => {
    const artifactKind = String(artifact.kind || '').toLowerCase();
    const isHtml = artifactKind === 'html' || artifactKind === 'document';
    const isMarkdown = artifactKind === 'markdown' || artifactKind === 'md' || artifactKind === 'text';
    const renderKind = isHtml ? 'html' : (isMarkdown ? 'markdown' : 'text');
    const rawBody = artifact.content || artifact.source_md || '';
    const cappedBody = rawBody.length > MAX_PREVIEW_BYTES ? rawBody.slice(0, MAX_PREVIEW_BYTES) : rawBody;
    return {
      ...artifact,
      is_protected: artifact.is_protected === 1,
      source_type: 'artifact',
      preview: capText(artifact.source_md || artifact.content || artifact.title, 1200),
      body: cappedBody,
      body_truncated: rawBody.length > MAX_PREVIEW_BYTES,
      render_kind: renderKind,
    };
  });
  const agents = db.prepare("SELECT id, name, status, engine, last_run_at FROM agents WHERE project_id = ? AND COALESCE(status, 'active') != 'archived' ORDER BY name ASC").all(project.id);
  const activeTasks = db.prepare("SELECT COUNT(*) AS count FROM tasks WHERE project_id = ? AND status IN ('todo','in_progress','in_review','blocked')").get(project.id)?.count || 0;
  const activeMissions = db.prepare("SELECT COUNT(*) AS count FROM missions WHERE project_id = ? AND status IN ('queued','running','in_progress')").get(project.id)?.count || 0;
  const recentRuns = db.prepare(`
    SELECT r.* FROM runs r
    LEFT JOIN agents a ON a.id = r.agent_id
    WHERE a.project_id = ?
    ORDER BY r.at DESC
    LIMIT 5
  `).all(project.id);
  const changelogLimit = Math.max(12, Math.min(500, Number(opts.changelogLimit) || 200));
  const changelog = listProjectChangelog(project.id, { limit: changelogLimit });
  const changelogByFolder = groupChangelogByFolder(changelog);
  return {
    project,
    folders,
    files,
    artifacts,
    libraryItems: [...files, ...artifacts]
      .sort((a, b) => String(b.updated_at || b.created_at || '').localeCompare(String(a.updated_at || a.created_at || '')))
      .slice(0, limit * 2),
    agents,
    activeTasks: Number(activeTasks),
    activeMissions: Number(activeMissions),
    recentRuns,
    changelog,
    changelogByFolder,
    counts: {
      folders: folders.length,
      files: db.prepare('SELECT COUNT(*) AS count FROM disk_files WHERE project_id = ?').get(project.id)?.count || 0,
      artifacts: db.prepare('SELECT COUNT(*) AS count FROM artifacts WHERE project_id = ?').get(project.id)?.count || 0,
      localOnly: db.prepare('SELECT COUNT(*) AS count FROM disk_files WHERE project_id = ? AND local_only = 1').get(project.id)?.count || 0,
      agents: agents.length,
      activeTasks: Number(activeTasks),
      activeMissions: Number(activeMissions),
    },
  };
}

export function createProject(input = {}) {
  const name = String(input.name || '').trim();
  if (!name) throw new Error('name is required');
  const db = getDb();
  const requested = input.slug ? slugifyProject(input.slug) : slugifyProject(name);
  if (input.slug && db.prepare('SELECT 1 FROM projects WHERE slug = ?').get(requested)) {
    const err = new Error('project slug already exists');
    err.code = 'PROJECT_SLUG_EXISTS';
    throw err;
  }
  const slug = input.slug ? requested : nextSlug(db, requested);
  const id = input.id || slug;
  const folderPath = resolveProjectFolderPath(input.folder_path, slug);
  ensureFolderOnDisk(folderPath);
  const now = nowIso();
  db.prepare(`
    INSERT INTO projects (id, slug, name, color, description, status, folder_path, created_at)
    VALUES (?, ?, ?, ?, ?, ?, ?, ?)
  `).run(id, slug, name, input.color || '#111318', input.description || null, input.status || 'active', folderPath, now);
  ensureProjectRootFolder(id, slug);
  return getProject(slug);
}

export function listProjects(opts = {}) {
  const db = getDb();
  const status = opts.status || 'active';
  if (status === 'all') return db.prepare('SELECT * FROM projects ORDER BY status ASC, created_at ASC').all().map(rowToProject);
  return db.prepare('SELECT * FROM projects WHERE status = ? ORDER BY created_at ASC').all(status).map(rowToProject);
}

export function getProject(idOrSlug) {
  const db = getDb();
  return rowToProject(db.prepare('SELECT * FROM projects WHERE id = ? OR slug = ? LIMIT 1').get(idOrSlug, idOrSlug));
}

export function updateProject(idOrSlug, fields = {}) {
  const project = getProject(idOrSlug);
  if (!project) return null;
  const allowed = ['name', 'color', 'description', 'status', 'folder_path'];
  const entries = Object.entries(fields).filter(([key, value]) => allowed.includes(key) && value !== undefined);
  if (!entries.length) return project;
  const sets = entries.map(([key]) => `${key} = ?`).join(', ');
  const values = entries.map(([, value]) => value);
  getDb().prepare(`UPDATE projects SET ${sets} WHERE id = ?`).run(...values, project.id);
  return getProject(project.id);
}

export function archiveProject(idOrSlug) {
  const project = getProject(idOrSlug);
  if (!project) return null;
  if (project.id === DEFAULT_PROJECT_ID) {
    const err = new Error('The Personal project is the default scope and cannot be archived.');
    err.code = 'PROJECT_PROTECTED';
    throw err;
  }
  return updateProject(idOrSlug, { status: 'archived' });
}

export function backfillArtifactProjectIds(db = getDb()) {
  try {
    const result = db.prepare(`UPDATE artifacts SET project_id = ? WHERE project_id IS NULL OR project_id = ''`).run(DEFAULT_PROJECT_ID);
    return result.changes || 0;
  } catch {
    return 0;
  }
}

export function deleteProject(idOrSlug) {
  const project = getProject(idOrSlug);
  if (!project) return { deleted: false, reason: 'not_found' };
  if (project.id === DEFAULT_PROJECT_ID) {
    const err = new Error('The Personal project is the default scope and cannot be deleted.');
    err.code = 'PROJECT_PROTECTED';
    throw err;
  }
  const db = getDb();
  db.exec('BEGIN');
  try {
    for (const t of PROJECT_CHILD_TABLES) {
      try {
        db.prepare(`DELETE FROM ${t} WHERE project_id = ?`).run(project.id);
      } catch (err) {
        if (!/no such (table|column)/i.test(String(err?.message || err))) throw err;
      }
    }
    db.prepare('DELETE FROM projects WHERE id = ?').run(project.id);
    db.exec('COMMIT');
  } catch (err) {
    try { db.exec('ROLLBACK'); } catch {}
    throw err;
  }
  try {
    const blobDir = path.join(PROJECT_BLOB_ROOT, project.slug);
    fs.rmSync(blobDir, { recursive: true, force: true });
  } catch {}
  return { deleted: true, project };
}

export function ensureDefaultProject(db = getDb()) {
  const personal = db.prepare("SELECT * FROM projects WHERE id = 'personal' OR slug = 'personal'").get();
  if (personal) {
    if (!personal.folder_path) {
      const folderPath = defaultProjectFolderPath(personal.slug);
      try { fs.mkdirSync(folderPath, { recursive: true }); } catch {}
      db.prepare('UPDATE projects SET folder_path = ? WHERE id = ?').run(folderPath, personal.id);
    }
    ensureProjectRootFolder(personal.id, personal.slug, db);
    backfillArtifactProjectIds(db);
    return rowToProject(db.prepare("SELECT * FROM projects WHERE id = 'personal'").get());
  }
  const alreadySeeded = (() => { try { return fs.existsSync(DEFAULT_SEED_MARKER); } catch { return false; } })();
  const anyProjects = db.prepare('SELECT 1 FROM projects LIMIT 1').get();
  if (alreadySeeded || anyProjects) return null;
  const now = nowIso();
  const folderPath = defaultProjectFolderPath(DEFAULT_PROJECT_ID);
  try { fs.mkdirSync(folderPath, { recursive: true }); } catch {}
  db.prepare(`INSERT INTO projects (id, slug, name, color, description, status, folder_path, created_at) VALUES (?, ?, ?, ?, ?, 'active', ?, ?)`)
    .run(DEFAULT_PROJECT_ID, DEFAULT_PROJECT_ID, DEFAULT_PROJECT_NAME, DEFAULT_PROJECT_COLOR, DEFAULT_PROJECT_DESC, folderPath, now);
  ensureProjectRootFolder(DEFAULT_PROJECT_ID, DEFAULT_PROJECT_ID, db);
  backfillArtifactProjectIds(db);
  try { fs.writeFileSync(DEFAULT_SEED_MARKER, now); } catch {}
  return rowToProject(db.prepare("SELECT * FROM projects WHERE id = ?").get(DEFAULT_PROJECT_ID));
}


export function ensureProjectRootFolder(projectId, slug, db = getDb()) {
  try {
    const existing = db.prepare('SELECT id FROM disk_folders WHERE project_id = ? AND parent_id IS NULL LIMIT 1').get(projectId);
    if (existing) return existing.id;
    const id = `df-root-${projectId}`.replace(/[^a-zA-Z0-9_-]/g, '-');
    const now = nowIso();
    db.prepare(`INSERT INTO disk_folders (id, project_id, parent_id, name, path, created_at, updated_at) VALUES (?, ?, NULL, ?, ?, ?, ?)`)
      .run(id, projectId, slug || projectId, `/${slug || projectId}/`, now, now);
    return id;
  } catch { return null; }
}
