import { execFile } from 'node:child_process';
import { randomUUID } from 'node:crypto';
import { createReadStream, existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, statSync, unlinkSync, writeFileSync } from 'node:fs';
import { homedir } from 'node:os';
import { basename, dirname, extname, isAbsolute, join, relative, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import { Readable } from 'node:stream';
import { buildCommitMessagePrompt } from './commit-message-generator.js';
import { errorDetails, logger } from './logging.js';
import { importExternalSessions, scanExternalSessionsFiltered } from './sessionImport.js';
import { listRegistrySources, resolveInstallFromRegistry, searchAcrossRegistries, RegistryError } from './extension-registries/index.js';
const MAX_TEXT_FILE_BYTES = 2 * 1024 * 1024;
const MIME_BY_EXTENSION = {
    avif: 'image/avif',
    bmp: 'image/bmp',
    css: 'text/css; charset=utf-8',
    gif: 'image/gif',
    htm: 'text/html; charset=utf-8',
    html: 'text/html; charset=utf-8',
    ico: 'image/x-icon',
    jpeg: 'image/jpeg',
    jpg: 'image/jpeg',
    js: 'text/javascript; charset=utf-8',
    json: 'application/json; charset=utf-8',
    m4v: 'video/mp4',
    md: 'text/markdown; charset=utf-8',
    mov: 'video/quicktime',
    mp4: 'video/mp4',
    ogg: 'video/ogg',
    png: 'image/png',
    svg: 'image/svg+xml; charset=utf-8',
    ts: 'text/typescript; charset=utf-8',
    txt: 'text/plain; charset=utf-8',
    webm: 'video/webm',
    webp: 'image/webp',
    xml: 'application/xml; charset=utf-8',
    yaml: 'application/yaml; charset=utf-8',
    yml: 'application/yaml; charset=utf-8'
};
function sendJson(res, status, body) {
    const payload = JSON.stringify(body);
    res.writeHead(status, {
        'content-type': 'application/json; charset=utf-8',
        'content-length': Buffer.byteLength(payload)
    });
    res.end(payload);
}
function sendError(res, status, message) {
    sendJson(res, status, { error: message });
}
async function readJson(req) {
    const chunks = [];
    for await (const chunk of req) {
        chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
    }
    if (chunks.length === 0)
        return {};
    return JSON.parse(Buffer.concat(chunks).toString('utf8'));
}
function stringBodyField(body, field) {
    return body && typeof body === 'object' && typeof body[field] === 'string'
        ? (body[field])
        : null;
}
function nullableStringBodyField(body, field) {
    if (!body || typeof body !== 'object' || !Object.hasOwn(body, field))
        return undefined;
    const value = body[field];
    if (value === null)
        return null;
    return typeof value === 'string' ? value : undefined;
}
function booleanBodyField(body, field) {
    if (!body || typeof body !== 'object' || !Object.hasOwn(body, field))
        return undefined;
    const value = body[field];
    return typeof value === 'boolean' ? value : undefined;
}
function interactionModeBodyField(body) {
    const value = nullableStringBodyField(body, 'interactionMode');
    if (value === undefined)
        return undefined;
    return value === 'plan' ? 'plan' : 'build';
}
function commitMessageProviderBodyField(body) {
    const value = nullableStringBodyField(body, 'commitMessageProvider');
    if (value === 'claude' || value === 'codex')
        return value;
    return undefined;
}
function timeFormatBodyField(body) {
    const value = nullableStringBodyField(body, 'timeFormat');
    if (value === '12h' || value === '24h')
        return value;
    return undefined;
}
function dateFormatBodyField(body) {
    const value = nullableStringBodyField(body, 'dateFormat');
    if (value === 'locale' || value === 'iso' || value === 'us' || value === 'eu' || value === 'long')
        return value;
    return undefined;
}
function readPackageVersion() {
    const packageJsonPath = fileURLToPath(new URL('../../../package.json', import.meta.url));
    const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
    return packageJson.version || 'unknown';
}
async function fetchLatestPackageVersion(current) {
    const registryUrl = 'https://registry.npmjs.org/fatmux/latest';
    try {
        const res = await fetch(registryUrl, {
            headers: { accept: 'application/json' }
        });
        if (!res.ok) {
            logger.warn('version.latest_fetch_failed', {
                url: registryUrl,
                current,
                status: res.status,
                statusText: res.statusText
            });
            return null;
        }
        const data = (await res.json());
        const latest = data.version?.trim() || null;
        return latest && latest !== current ? latest : latest;
    }
    catch (error) {
        logger.warn('version.latest_fetch_failed', {
            url: registryUrl,
            current,
            ...errorDetails(error)
        });
        return null;
    }
}
function telegramBotTokenBodyField(body) {
    const value = nullableStringBodyField(body, 'telegramBotToken');
    if (value === undefined)
        return undefined;
    return value?.trim() || null;
}
function telegramChatIdBodyField(body) {
    const value = nullableStringBodyField(body, 'telegramChatId');
    if (value === undefined)
        return undefined;
    return value?.trim() || null;
}
function stringArrayBodyField(body, field) {
    const value = body && typeof body === 'object' ? body[field] : null;
    return Array.isArray(value) ? value.filter((item) => typeof item === 'string') : [];
}
function archivedQuery(url) {
    const value = url.searchParams.get('archived');
    if (value === 'all')
        return 'all';
    return value === 'true';
}
function isRunningSession(session) {
    return session.status === 'running' || session.status === 'queued';
}
function isUnreadSession(session) {
    if (session.archived || isRunningSession(session))
        return false;
    const updatedAt = Date.parse(session.updatedAt);
    const openedAt = session.lastOpenedAt ? Date.parse(session.lastOpenedAt) : Number.NaN;
    if (!Number.isFinite(updatedAt))
        return true;
    if (!Number.isFinite(openedAt))
        return true;
    return updatedAt > openedAt;
}
function safeFileName(value) {
    return basename(value).replace(/[^\w.-]+/g, '_') || 'upload';
}
function isInsideDirectory(root, child) {
    const rel = relative(root, child);
    return rel === '' || (!rel.startsWith('..') && !isAbsolute(rel));
}
function projectRelativePath(root, absolutePath) {
    return relative(root, absolutePath).split('\\').join('/');
}
function resolveProjectPath(root, rawPath) {
    const normalized = rawPath?.trim() || '.';
    if (normalized.includes('\0'))
        return null;
    const resolved = resolve(root, normalized);
    return isInsideDirectory(root, resolved) ? resolved : null;
}
function isProbablyText(buffer) {
    if (buffer.includes(0))
        return false;
    const sample = buffer.subarray(0, Math.min(buffer.byteLength, 4096));
    let suspicious = 0;
    for (const byte of sample) {
        if (byte < 7 || (byte > 14 && byte < 32))
            suspicious += 1;
    }
    return sample.byteLength === 0 || suspicious / sample.byteLength < 0.08;
}
function mimeTypeForPath(filePath) {
    const extension = extname(filePath).slice(1).toLowerCase();
    return MIME_BY_EXTENSION[extension] ?? 'application/octet-stream';
}
function contentDisposition(kind, fileName) {
    const fallback = safeFileName(fileName).replaceAll('"', '_');
    return `${kind}; filename="${fallback}"; filename*=UTF-8''${encodeURIComponent(fileName)}`;
}
function parseByteRange(rangeHeader, size) {
    const match = /^bytes=(\d*)-(\d*)$/.exec(rangeHeader.trim());
    if (!match)
        return null;
    const rawStart = match[1];
    const rawEnd = match[2];
    if (!rawStart && !rawEnd)
        return null;
    let start;
    let end;
    if (!rawStart) {
        const suffixLength = Number(rawEnd);
        if (!Number.isSafeInteger(suffixLength) || suffixLength <= 0)
            return null;
        start = Math.max(size - suffixLength, 0);
        end = size - 1;
    }
    else {
        start = Number(rawStart);
        end = rawEnd ? Number(rawEnd) : size - 1;
    }
    if (!Number.isSafeInteger(start) || !Number.isSafeInteger(end) || start < 0 || end < start || start >= size) {
        return null;
    }
    return { start, end: Math.min(end, size - 1) };
}
function streamProjectFile(req, res, filePath, asDownload) {
    const stats = statSync(filePath);
    const range = req.headers.range ? parseByteRange(String(req.headers.range), stats.size) : null;
    const fileName = basename(filePath);
    const headers = {
        'accept-ranges': 'bytes',
        'content-type': mimeTypeForPath(filePath),
        'content-disposition': contentDisposition(asDownload ? 'attachment' : 'inline', fileName)
    };
    if (req.headers.range && !range) {
        res.writeHead(416, { ...headers, 'content-range': `bytes */${stats.size}` });
        res.end();
        return;
    }
    if (range) {
        headers['content-length'] = range.end - range.start + 1;
        headers['content-range'] = `bytes ${range.start}-${range.end}/${stats.size}`;
        res.writeHead(206, headers);
        createReadStream(filePath, { start: range.start, end: range.end }).pipe(res);
        return;
    }
    headers['content-length'] = stats.size;
    res.writeHead(200, headers);
    createReadStream(filePath).pipe(res);
}
function gitCheckIgnored(cwd, paths) {
    const candidates = paths.filter(Boolean);
    if (candidates.length === 0)
        return Promise.resolve(new Set());
    return new Promise((resolveIgnored) => {
        const child = execFile('git', ['check-ignore', '-z', '--stdin'], {
            cwd,
            encoding: 'buffer',
            maxBuffer: 10 * 1024 * 1024,
            timeout: 10_000
        }, (error, stdout) => {
            const code = error ? error.code : 0;
            if (error && code !== 1) {
                resolveIgnored(new Set());
                return;
            }
            const ignored = new Set(stdout
                .toString('utf8')
                .split('\0')
                .filter(Boolean)
                .map((item) => item.replace(/\/$/, '')));
            resolveIgnored(ignored);
        });
        child.stdin?.end(candidates.join('\0') + '\0');
    });
}
async function readFormData(req) {
    const headers = new Headers();
    for (const [key, value] of Object.entries(req.headers)) {
        if (Array.isArray(value)) {
            for (const item of value)
                headers.append(key, item);
        }
        else if (value !== undefined) {
            headers.set(key, value);
        }
    }
    const request = new Request('http://localhost', {
        method: req.method,
        headers,
        body: Readable.toWeb(req),
        duplex: 'half'
    });
    return request.formData();
}
export class ApiHandler {
    deps;
    importInFlight = false;
    projectSummaryInFlight = new Map();
    constructor(deps) {
        this.deps = deps;
    }
    async handle(req, res) {
        if (!req.url)
            return false;
        const url = new URL(req.url, 'http://localhost');
        if (!url.pathname.startsWith('/api/'))
            return false;
        try {
            await this.route(req, res, url);
        }
        catch (error) {
            sendError(res, 500, error instanceof Error ? error.message : String(error));
        }
        return true;
    }
    async route(req, res, url) {
        const method = req.method || 'GET';
        const parts = url.pathname.split('/').filter(Boolean);
        if (method === 'GET' && url.pathname === '/api/agent-providers') {
            sendJson(res, 200, await this.deps.agents.providers());
            return;
        }
        if (method === 'GET' && url.pathname === '/api/provider-usage') {
            sendJson(res, 200, await this.deps.usage.getSummary());
            return;
        }
        if (method === 'GET' && url.pathname === '/api/version') {
            const current = readPackageVersion();
            const latest = await fetchLatestPackageVersion(current);
            const body = {
                current,
                latest,
                updateAvailable: Boolean(latest && latest !== current)
            };
            sendJson(res, 200, body);
            return;
        }
        if (url.pathname === '/api/settings') {
            await this.handleSettings(req, res);
            return;
        }
        if (method === 'GET' && parts[1] === 'agent-sessions' && parts[3] === 'messages') {
            sendJson(res, 200, this.deps.db.listAgentMessages(parts[2]));
            return;
        }
        if (parts[1] === 'agent-sessions' && parts[3] === 'attachments') {
            await this.handleAgentAttachments(req, res, parts);
            return;
        }
        if (parts[1] === 'agent-sessions' && parts.length === 3 && method === 'PATCH') {
            const body = await readJson(req);
            const updated = this.deps.agents.updateSession(parts[2], {
                title: nullableStringBodyField(body, 'title') ?? undefined,
                archived: booleanBodyField(body, 'archived'),
                model: nullableStringBodyField(body, 'model'),
                permissionMode: nullableStringBodyField(body, 'permissionMode'),
                interactionMode: interactionModeBodyField(body),
                effort: nullableStringBodyField(body, 'effort')
            });
            sendJson(res, 200, updated);
            return;
        }
        if (parts[1] === 'agent-sessions' && parts.length === 4 && parts[3] === 'read' && method === 'POST') {
            const session = this.deps.agents.markSessionRead(parts[2]);
            sendJson(res, 200, session);
            return;
        }
        if (parts[1] === 'fs') {
            if (method === 'GET' && parts[2] === 'browse') {
                const rawPath = url.searchParams.get('path') || homedir();
                const absPath = resolve(rawPath);
                if (!existsSync(absPath) || !statSync(absPath).isDirectory()) {
                    return sendError(res, 400, 'Not a directory');
                }
                let entries = [];
                try {
                    entries = readdirSync(absPath, { withFileTypes: true })
                        .filter((d) => d.isDirectory() && !d.name.startsWith('.'))
                        .map((d) => ({ name: d.name, path: join(absPath, d.name) }))
                        .sort((a, b) => a.name.localeCompare(b.name));
                }
                catch {
                    return sendError(res, 403, 'Permission denied');
                }
                const parent = absPath !== '/' ? resolve(absPath, '..') : null;
                sendJson(res, 200, { path: absPath, parent, entries });
                return;
            }
            if (method === 'POST' && parts[2] === 'mkdir') {
                const body = await readJson(req);
                const rawParent = stringBodyField(body, 'parent');
                const name = stringBodyField(body, 'name');
                if (!rawParent || !name)
                    return sendError(res, 400, 'parent and name required');
                if (/[\/\0]/.test(name))
                    return sendError(res, 400, 'Invalid folder name');
                const newPath = join(resolve(rawParent), name);
                if (existsSync(newPath))
                    return sendError(res, 400, 'Folder already exists');
                mkdirSync(newPath, { recursive: true });
                sendJson(res, 201, { path: newPath });
                return;
            }
            return sendError(res, 404, 'Not found');
        }
        if (parts[1] === 'external-sessions') {
            if (method === 'GET' && parts[2] === 'scan' && parts.length === 3) {
                const cwd = url.searchParams.get('cwd') || undefined;
                const projectId = url.searchParams.get('projectId') || undefined;
                if (cwd && projectId) {
                    return sendError(res, 400, 'specify cwd OR projectId, not both');
                }
                const groups = await scanExternalSessionsFiltered({ db: this.deps.db }, { cwd, projectId });
                sendJson(res, 200, groups);
                return;
            }
            if (method === 'POST' && parts[2] === 'import' && parts.length === 3) {
                if (this.importInFlight) {
                    return sendError(res, 409, 'an import is already running');
                }
                const body = (await readJson(req));
                const cwd = typeof body?.cwd === 'string' ? body.cwd : undefined;
                const projectId = typeof body?.projectId === 'string' ? body.projectId : undefined;
                if (cwd && projectId) {
                    return sendError(res, 400, 'specify cwd OR projectId, not both');
                }
                this.importInFlight = true;
                try {
                    const result = await importExternalSessions({ db: this.deps.db, git: this.deps.git }, { cwd, projectId });
                    sendJson(res, 200, result);
                }
                catch (err) {
                    logger.warn('externalSessions.import_failed', errorDetails(err));
                    sendError(res, 500, err instanceof Error ? err.message : 'import failed');
                }
                finally {
                    this.importInFlight = false;
                }
                return;
            }
            return sendError(res, 404, 'Not found');
        }
        if (parts[1] === 'project-import-blocks') {
            if (method === 'GET' && parts.length === 2) {
                sendJson(res, 200, this.deps.db.listProjectImportBlocks());
                return;
            }
            if (method === 'POST' && parts.length === 2) {
                const body = await readJson(req);
                const rawPath = stringBodyField(body, 'path');
                if (!rawPath)
                    return sendError(res, 400, 'path is required');
                sendJson(res, 201, this.deps.db.blockProjectImportPath(rawPath));
                return;
            }
            if (method === 'DELETE' && parts.length === 2) {
                const rawPath = url.searchParams.get('path');
                if (!rawPath)
                    return sendError(res, 400, 'path is required');
                sendJson(res, this.deps.db.unblockProjectImportPath(rawPath) ? 200 : 404, { ok: true });
                return;
            }
            return sendError(res, 404, 'Not found');
        }
        if (parts[1] === 'extensions') {
            await this.handleExtensions(req, res, url, parts);
            return;
        }
        if (parts[1] === 'agent-sessions' && parts[3] === 'extensions') {
            await this.handleSessionExtensions(req, res, parts);
            return;
        }
        if (parts[1] !== 'projects') {
            sendError(res, 404, 'Not found');
            return;
        }
        if (parts.length === 2 && method === 'GET') {
            sendJson(res, 200, await this.projectSummaries());
            return;
        }
        if (parts.length === 2 && method === 'POST') {
            const body = await readJson(req);
            const rawPath = stringBodyField(body, 'path');
            if (!rawPath)
                return sendError(res, 400, 'path is required');
            const path = resolve(rawPath);
            if (!existsSync(path) || !statSync(path).isDirectory()) {
                return sendError(res, 400, `Directory does not exist: ${path}`);
            }
            const isGitRepo = await this.deps.git.isRepo(path);
            const project = this.deps.db.addProject({
                path,
                name: path.split('/').pop() || path,
                isGitRepo
            });
            sendJson(res, 201, project);
            return;
        }
        const projectId = parts[2];
        const project = projectId ? this.deps.db.getProject(projectId) : null;
        if (!project) {
            sendError(res, 404, 'Project not found');
            return;
        }
        if (parts.length === 3 && method === 'DELETE') {
            if (url.searchParams.get('blockImport') === 'true') {
                this.deps.db.blockProjectImportPath(project.path);
            }
            sendJson(res, this.deps.db.removeProject(project.id) ? 200 : 404, { ok: true });
            return;
        }
        if (parts[3] === 'summary' && method === 'GET') {
            sendJson(res, 200, await this.projectSummary(project.id));
            return;
        }
        if (parts[3] === 'terminals' && method === 'GET') {
            sendJson(res, 200, this.deps.pty.list(project.id));
            return;
        }
        if (parts[3] === 'agent-sessions') {
            if (method === 'GET') {
                sendJson(res, 200, this.deps.db.listAgentSessions(project.id, archivedQuery(url)));
                return;
            }
            if (method === 'POST') {
                const body = await readJson(req);
                const provider = stringBodyField(body, 'provider');
                if (provider !== 'claude' && provider !== 'codex') {
                    return sendError(res, 400, 'provider must be claude or codex');
                }
                const title = stringBodyField(body, 'title') || (provider === 'claude' ? 'Claude' : 'Codex');
                const model = nullableStringBodyField(body, 'model')?.trim() || null;
                const permissionMode = nullableStringBodyField(body, 'permissionMode')?.trim() || null;
                const interactionMode = interactionModeBodyField(body);
                const effort = nullableStringBodyField(body, 'effort')?.trim() || null;
                const session = this.deps.db.createAgentSession({
                    projectId: project.id,
                    provider,
                    title,
                    model,
                    permissionMode,
                    interactionMode,
                    effort
                });
                sendJson(res, 201, session);
                return;
            }
        }
        if (parts[3] === 'git') {
            await this.handleGit(req, res, url, project.path, project.id, parts[4]);
            return;
        }
        if (parts[3] === 'files') {
            await this.handleProjectFiles(req, res, url, project.path, parts[4]);
            return;
        }
        if (parts[3] === 'extensions') {
            await this.handleProjectExtensions(req, res, url, project.id, project.path);
            return;
        }
        sendError(res, 404, 'Not found');
    }
    async handleExtensions(req, res, url, parts) {
        const method = req.method || 'GET';
        if (method === 'GET' && parts.length === 2) {
            sendJson(res, 200, {
                skills: this.deps.extensions.listInstalledSkills(),
                mcpServers: this.deps.extensions.listInstalledMcpServers(),
                registries: listRegistrySources()
            });
            return;
        }
        if (method === 'GET' && parts[2] === 'registry') {
            const q = url.searchParams.get('q') || '';
            const kindParam = url.searchParams.get('kind');
            const kind = kindParam === 'skill' || kindParam === 'mcp' ? kindParam : undefined;
            const sourcesParam = url.searchParams.get('sources');
            const sources = sourcesParam ? sourcesParam.split(',').filter(Boolean) : undefined;
            try {
                const entries = await searchAcrossRegistries(q, { kind, sources });
                sendJson(res, 200, { entries });
            }
            catch (error) {
                if (error instanceof RegistryError) {
                    sendError(res, error.statusCode, error.message);
                    return;
                }
                throw error;
            }
            return;
        }
        if (method === 'POST' && parts[2] === 'install' && parts.length === 3) {
            const body = (await readJson(req));
            if (!body.source || !body.entry) {
                return sendError(res, 400, 'source and entry are required');
            }
            try {
                const spec = await resolveInstallFromRegistry(body.source, body.entry);
                if (body.entry.kind === 'skill') {
                    const installed = this.deps.extensions.installSkill(spec);
                    sendJson(res, 200, installed);
                }
                else {
                    const installed = this.deps.extensions.installMcpServer(spec);
                    sendJson(res, 200, installed);
                }
            }
            catch (error) {
                if (error instanceof RegistryError) {
                    sendError(res, error.statusCode, error.message);
                    return;
                }
                throw error;
            }
            return;
        }
        if (method === 'DELETE' && parts.length === 4) {
            const kindParam = parts[2];
            if (kindParam !== 'skill' && kindParam !== 'mcp') {
                return sendError(res, 400, 'kind must be skill or mcp');
            }
            this.deps.extensions.uninstall(kindParam, decodeURIComponent(parts[3]));
            sendJson(res, 200, { ok: true });
            return;
        }
        sendError(res, 404, 'Not found');
    }
    async handleProjectExtensions(req, res, _url, projectId, projectPath) {
        const method = req.method || 'GET';
        if (method === 'GET') {
            sendJson(res, 200, this.deps.db.listProjectExtensions(projectId));
            return;
        }
        if (method === 'POST') {
            const body = (await readJson(req));
            if (body.kind !== 'skill' && body.kind !== 'mcp') {
                return sendError(res, 400, 'kind must be skill or mcp');
            }
            if (!body.name)
                return sendError(res, 400, 'name is required');
            if (typeof body.enabled !== 'boolean')
                return sendError(res, 400, 'enabled is required');
            this.deps.extensions.setProjectExtension(projectId, projectPath, body.kind, body.name, body.enabled);
            sendJson(res, 200, { ok: true });
            return;
        }
        sendError(res, 405, 'Method not allowed');
    }
    async handleSessionExtensions(req, res, parts) {
        const sessionId = parts[2];
        const session = this.deps.db.getAgentSession(sessionId);
        if (!session)
            return sendError(res, 404, 'Agent session not found');
        const method = req.method || 'GET';
        if (method === 'GET') {
            sendJson(res, 200, this.deps.extensions.sessionExtensions(sessionId, session.projectId));
            return;
        }
        if (method === 'POST') {
            const body = (await readJson(req));
            if (body.kind !== 'skill' && body.kind !== 'mcp') {
                return sendError(res, 400, 'kind must be skill or mcp');
            }
            if (!body.name)
                return sendError(res, 400, 'name is required');
            this.deps.extensions.setSessionOverride({
                sessionId,
                kind: body.kind,
                name: body.name,
                skillState: body.skillState,
                mcpEnabled: body.mcpEnabled
            });
            sendJson(res, 200, this.deps.extensions.sessionExtensions(sessionId, session.projectId));
            return;
        }
        sendError(res, 405, 'Method not allowed');
    }
    async handleProjectFiles(req, res, url, projectPath, action) {
        const method = req.method || 'GET';
        const root = resolve(projectPath);
        if (!action && method === 'GET') {
            const directory = resolveProjectPath(root, url.searchParams.get('path'));
            if (!directory)
                return sendError(res, 400, 'Invalid path');
            if (!existsSync(directory) || !statSync(directory).isDirectory()) {
                return sendError(res, 400, 'Not a directory');
            }
            const showHidden = url.searchParams.get('hidden') === 'true';
            let dirents;
            try {
                dirents = readdirSync(directory, { withFileTypes: true });
            }
            catch {
                return sendError(res, 403, 'Permission denied');
            }
            const visibleDirents = dirents.filter((entry) => showHidden || !entry.name.startsWith('.'));
            const rawEntries = visibleDirents
                .map((entry) => {
                const absolutePath = join(directory, entry.name);
                const stats = lstatSync(absolutePath);
                const type = entry.isDirectory() ? 'directory' : 'file';
                const rel = projectRelativePath(root, absolutePath);
                const ignoreCandidate = type === 'directory' ? `${rel}/` : rel;
                return { entry, stats, type, rel, ignoreCandidate };
            })
                .filter((item) => item.type === 'directory' || item.entry.isFile());
            const ignored = await gitCheckIgnored(root, rawEntries.map((item) => item.ignoreCandidate));
            const entries = rawEntries
                .map((item) => ({
                name: item.entry.name,
                path: item.rel,
                type: item.type,
                extension: item.type === 'file' ? extname(item.entry.name).slice(1).toLowerCase() || null : null,
                size: item.stats.size,
                hidden: item.entry.name.startsWith('.'),
                ignored: ignored.has(item.rel) ||
                    (item.type === 'directory' && ignored.has(item.ignoreCandidate.replace(/\/$/, ''))),
                modifiedAt: item.stats.mtime.toISOString()
            }))
                .sort((a, b) => {
                if (a.type !== b.type)
                    return a.type === 'directory' ? -1 : 1;
                return a.name.localeCompare(b.name);
            });
            const currentPath = projectRelativePath(root, directory);
            const parentPath = directory === root ? null : projectRelativePath(root, dirname(directory));
            sendJson(res, 200, { path: currentPath, parentPath, entries });
            return;
        }
        if ((action === 'raw' || action === 'download') && method === 'GET') {
            const filePath = resolveProjectPath(root, url.searchParams.get('path'));
            if (!filePath)
                return sendError(res, 400, 'Invalid path');
            if (!existsSync(filePath) || !statSync(filePath).isFile()) {
                return sendError(res, 400, 'Not a file');
            }
            streamProjectFile(req, res, filePath, action === 'download');
            return;
        }
        if (action === 'content' && method === 'GET') {
            const filePath = resolveProjectPath(root, url.searchParams.get('path'));
            if (!filePath)
                return sendError(res, 400, 'Invalid path');
            if (!existsSync(filePath) || !statSync(filePath).isFile()) {
                return sendError(res, 400, 'Not a file');
            }
            const stats = statSync(filePath);
            if (stats.size > MAX_TEXT_FILE_BYTES) {
                return sendError(res, 413, 'File is too large to edit');
            }
            const bytes = readFileSync(filePath);
            if (!isProbablyText(bytes)) {
                return sendError(res, 415, 'Binary files cannot be edited here');
            }
            sendJson(res, 200, {
                path: projectRelativePath(root, filePath),
                name: basename(filePath),
                content: bytes.toString('utf8'),
                size: stats.size,
                modifiedAt: stats.mtime.toISOString()
            });
            return;
        }
        if (action === 'content' && method === 'PUT') {
            const body = await readJson(req);
            const rawPath = stringBodyField(body, 'path');
            const content = stringBodyField(body, 'content');
            if (!rawPath || content === null)
                return sendError(res, 400, 'path and content required');
            const filePath = resolveProjectPath(root, rawPath);
            if (!filePath)
                return sendError(res, 400, 'Invalid path');
            if (!existsSync(filePath) || !statSync(filePath).isFile()) {
                return sendError(res, 400, 'Not a file');
            }
            writeFileSync(filePath, content, 'utf8');
            sendJson(res, 200, { ok: true, path: projectRelativePath(root, filePath) });
            return;
        }
        if (action === 'upload' && method === 'POST') {
            const directory = resolveProjectPath(root, url.searchParams.get('path'));
            if (!directory)
                return sendError(res, 400, 'Invalid path');
            if (!existsSync(directory) || !statSync(directory).isDirectory()) {
                return sendError(res, 400, 'Not a directory');
            }
            const form = await readFormData(req);
            const files = form
                .getAll('files')
                .filter((value) => typeof value === 'object' && value !== null && 'arrayBuffer' in value && 'name' in value);
            if (files.length === 0)
                return sendError(res, 400, 'files are required');
            const uploaded = [];
            for (const file of files) {
                const cleanName = safeFileName(file.name || 'upload');
                const storedPath = join(directory, cleanName);
                if (!isInsideDirectory(root, storedPath))
                    return sendError(res, 400, 'Invalid upload path');
                const bytes = Buffer.from(await file.arrayBuffer());
                writeFileSync(storedPath, bytes);
                uploaded.push({ name: cleanName, path: projectRelativePath(root, storedPath), size: bytes.byteLength });
            }
            sendJson(res, 201, { uploaded });
            return;
        }
        sendError(res, 404, 'Not found');
    }
    async handleAgentAttachments(req, res, parts) {
        const method = req.method || 'GET';
        const sessionId = parts[2];
        const session = this.deps.db.getAgentSession(sessionId);
        if (!session)
            return sendError(res, 404, 'Agent session not found');
        if (parts.length === 4 && method === 'GET') {
            sendJson(res, 200, this.deps.db.listAgentAttachments(sessionId));
            return;
        }
        if (parts.length === 5 && parts[4] === 'upload' && method === 'POST') {
            const form = await readFormData(req);
            const value = form.get('file');
            if (!value || typeof value !== 'object' || !('arrayBuffer' in value) || !('name' in value)) {
                return sendError(res, 400, 'file is required');
            }
            const file = value;
            const dir = join(this.deps.db.home, 'attachments', sessionId);
            mkdirSync(dir, { recursive: true });
            const cleanName = safeFileName(file.name || 'upload');
            const storedPath = join(dir, `${Date.now()}-${randomUUID()}-${cleanName}`);
            const bytes = Buffer.from(await file.arrayBuffer());
            writeFileSync(storedPath, bytes);
            const attachment = this.deps.db.createAgentAttachment({
                sessionId,
                kind: 'local_upload',
                name: cleanName,
                path: storedPath,
                size: bytes.byteLength,
                mimeType: file.type || null
            });
            sendJson(res, 201, attachment);
            return;
        }
        if (parts.length === 5 && parts[4] === 'project-file' && method === 'POST') {
            const body = await readJson(req);
            const rawPath = stringBodyField(body, 'path') || stringBodyField(body, 'filePath');
            if (!rawPath)
                return sendError(res, 400, 'path is required');
            const project = this.deps.db.getProject(session.projectId);
            if (!project)
                return sendError(res, 404, 'Project not found');
            const projectRoot = resolve(project.path);
            const absolutePath = resolve(projectRoot, rawPath);
            if (!isInsideDirectory(projectRoot, absolutePath)) {
                return sendError(res, 400, 'File must be inside the selected project');
            }
            if (!existsSync(absolutePath) || !statSync(absolutePath).isFile()) {
                return sendError(res, 400, `File does not exist: ${rawPath}`);
            }
            const stat = statSync(absolutePath);
            const name = relative(projectRoot, absolutePath) || basename(absolutePath);
            const attachment = this.deps.db.createAgentAttachment({
                sessionId,
                kind: 'project_file',
                name,
                path: absolutePath,
                size: stat.size,
                mimeType: null
            });
            sendJson(res, 201, attachment);
            return;
        }
        if (parts.length === 5 && method === 'DELETE') {
            const attachment = this.deps.db.deleteAgentAttachment(sessionId, parts[4]);
            if (!attachment)
                return sendError(res, 404, 'Attachment not found');
            if (attachment.kind === 'local_upload' && existsSync(attachment.path)) {
                try {
                    unlinkSync(attachment.path);
                }
                catch { }
            }
            sendJson(res, 200, { ok: true });
            return;
        }
        sendError(res, 404, 'Not found');
    }
    async handleGit(req, res, url, cwd, projectId, action) {
        const method = req.method || 'GET';
        if (method === 'GET' && action === 'status') {
            sendJson(res, 200, await this.deps.git.status(cwd));
            return;
        }
        if (method === 'GET' && action === 'branches') {
            sendJson(res, 200, await this.deps.git.branches(cwd));
            return;
        }
        if (method === 'GET' && action === 'log') {
            sendJson(res, 200, await this.deps.git.log(cwd));
            return;
        }
        if (method === 'GET' && action === 'diff') {
            const filePath = url.searchParams.get('filePath');
            if (!filePath)
                return sendError(res, 400, 'filePath is required');
            const staged = url.searchParams.get('staged') === 'true';
            sendJson(res, 200, { filePath, content: await this.deps.git.diff(cwd, filePath, staged) });
            return;
        }
        if (method === 'POST' && action === 'commit-message') {
            const status = await this.deps.git.status(cwd);
            if (status.staged.length === 0) {
                sendError(res, 400, 'No staged files');
                return;
            }
            const prompt = buildCommitMessagePrompt(status.staged, await this.deps.git.stagedDiff(cwd));
            const settings = this.deps.db.getAppSettings();
            const message = await this.deps.commitMessages.generate({
                cwd,
                provider: settings.commitMessageProvider,
                model: settings.commitMessageModel,
                prompt
            });
            sendJson(res, 200, { message });
            return;
        }
        if (method !== 'POST') {
            sendError(res, 405, 'Method not allowed');
            return;
        }
        const body = await readJson(req);
        let result = { ok: true };
        switch (action) {
            case 'stage':
                await this.deps.git.stage(cwd, stringArrayBodyField(body, 'files'));
                break;
            case 'unstage':
                await this.deps.git.unstage(cwd, stringArrayBodyField(body, 'files'));
                break;
            case 'commit': {
                const message = stringBodyField(body, 'message');
                if (!message)
                    return sendError(res, 400, 'message is required');
                result = { hash: await this.deps.git.commit(cwd, message) };
                break;
            }
            case 'checkout': {
                const branch = stringBodyField(body, 'branch');
                if (!branch)
                    return sendError(res, 400, 'branch is required');
                await this.deps.git.checkout(cwd, branch);
                break;
            }
            case 'branch': {
                const branch = stringBodyField(body, 'branch');
                if (!branch)
                    return sendError(res, 400, 'branch is required');
                await this.deps.git.createBranch(cwd, branch);
                break;
            }
            case 'fetch':
                result = { output: await this.deps.git.fetch(cwd) };
                break;
            case 'pull':
                result = { output: await this.deps.git.pull(cwd) };
                break;
            case 'push':
                result = { output: await this.deps.git.push(cwd) };
                break;
            default:
                return sendError(res, 404, 'Unknown git action');
        }
        const status = await this.deps.git.status(cwd);
        await this.deps.broadcastGitStatus(projectId);
        sendJson(res, 200, { ...(result || {}), status });
    }
    async handleSettings(req, res) {
        const method = req.method || 'GET';
        if (method === 'GET') {
            sendJson(res, 200, this.deps.db.getAppSettings());
            return;
        }
        if (method !== 'PATCH') {
            sendError(res, 405, 'Method not allowed');
            return;
        }
        const body = await readJson(req);
        const updated = this.deps.db.updateAppSettings({
            commitMessageProvider: commitMessageProviderBodyField(body),
            commitMessageModel: nullableStringBodyField(body, 'commitMessageModel') ?? undefined,
            timeFormat: timeFormatBodyField(body),
            dateFormat: dateFormatBodyField(body),
            telegramBotToken: telegramBotTokenBodyField(body),
            telegramChatId: telegramChatIdBodyField(body)
        });
        sendJson(res, 200, updated);
    }
    async projectSummaries() {
        return Promise.all(this.deps.db.listProjects().map((project) => this.projectSummary(project.id)));
    }
    async projectSummary(projectId) {
        const existing = this.projectSummaryInFlight.get(projectId);
        if (existing)
            return existing;
        const request = this.computeProjectSummary(projectId);
        this.projectSummaryInFlight.set(projectId, request);
        try {
            return await request;
        }
        finally {
            this.projectSummaryInFlight.delete(projectId);
        }
    }
    async computeProjectSummary(projectId) {
        let project = this.deps.db.getProject(projectId);
        if (!project)
            throw new Error('Project not found');
        let git = null;
        if (project.isGitRepo) {
            try {
                git = await this.deps.git.status(project.path);
            }
            catch {
                project = this.deps.db.updateProjectGitFlag(project.id, false) ?? project;
            }
        }
        else if (await this.deps.git.isRepo(project.path)) {
            project = this.deps.db.updateProjectGitFlag(project.id, true) ?? project;
            git = await this.deps.git.status(project.path);
        }
        const sessions = this.deps.db.listAgentSessions(project.id, 'all');
        return {
            ...project,
            git,
            activeTerminals: this.deps.pty.activeCount(project.id),
            activeAgentSessions: sessions.filter((session) => isRunningSession(session)).length,
            unreadAgentSessions: sessions.filter((session) => isUnreadSession(session)).length,
            lastActivity: this.deps.db.lastProjectActivity(project.id)
        };
    }
}
//# sourceMappingURL=api.js.map