import type {
	ApiEnvelope,
	App,
	Container,
	ContainerStats,
	ContainerStatsSample,
	ContainerView,
	SystemStats,
	SystemStatsSample,
	TopContainerSample,
	DockerHealth,
	ProxyHealth,
	EventLogEntry,
	EventLogStats,
	InspectResult,
	LocalImage,
	NpmCertificate,
	NpmAccessList,
	ProxyRoute,
	Registry,
	RegistryImage,
	Settings,
	StaleContainer,
	VolumeScopeInfo,
	BrowseResult,
	DnsZone,
	DnsRecordView,
	BackupInfo,
	Workload,
	WorkloadKind
} from './types';

// ── Helpers ─────────────────────────────────────────────────────────

export class ApiError extends Error {
	constructor(
		message: string,
		public readonly status: number
	) {
		super(message);
		this.name = 'ApiError';
	}
}

import { getAuthToken, clearAuth } from './auth';

// ── Concurrency limiter ────────────────────────────────────────────
// Chrome allows only 6 concurrent HTTP/1.1 connections per host.
// Reserve slots for the persistent SSE stream and health checks by
// capping regular API requests to 4 concurrent.
const MAX_CONCURRENT = 4;
let inflight = 0;
const queue: Array<() => void> = [];

function acquireSlot(signal?: AbortSignal | null): Promise<void> {
	if (inflight < MAX_CONCURRENT) {
		inflight++;
		return Promise.resolve();
	}
	return new Promise<void>((resolve, reject) => {
		// A queued waiter inherits the releasing request's slot, so it
		// must not increment `inflight` again — `releaseSlot` skips the
		// decrement when it hands the slot off, keeping the count stable.
		const entry = () => { resolve(); };
		queue.push(entry);

		signal?.addEventListener('abort', () => {
			const idx = queue.indexOf(entry);
			if (idx !== -1) {
				queue.splice(idx, 1);
				reject(signal.reason ?? new DOMException('Aborted', 'AbortError'));
			}
		}, { once: true });
	});
}

function releaseSlot(): void {
	const next = queue.shift();
	if (next) {
		next();
	} else {
		inflight--;
	}
}

async function request<T>(path: string, init?: RequestInit): Promise<T> {
	// Write operations (user-initiated) bypass the concurrency limiter
	// so they are never blocked behind background polling.
	const method = init?.method?.toUpperCase() ?? 'GET';
	if (method !== 'GET') {
		return requestInner<T>(path, init);
	}
	await acquireSlot(init?.signal);
	try {
		return await requestInner<T>(path, init);
	} finally {
		releaseSlot();
	}
}

async function requestInner<T>(path: string, init?: RequestInit): Promise<T> {
	const token = getAuthToken();
	const headers: Record<string, string> = {
		'Content-Type': 'application/json',
		...(init?.headers as Record<string, string>)
	};
	if (token) {
		headers['Authorization'] = `Bearer ${token}`;
	}

	const res = await fetch(path, {
		...init,
		headers
	});

	// Redirect to login on 401 (expired/missing token).
	if (res.status === 401 && typeof window !== 'undefined' && !path.includes('/auth/')) {
		clearAuth();
		window.location.href = '/login';
		throw new ApiError('Authentication required', 401);
	}

	let envelope: ApiEnvelope<T>;
	try {
		envelope = await res.json();
	} catch {
		throw new ApiError(
			`Server returned non-JSON response (HTTP ${res.status})`,
			res.status
		);
	}

	if (!envelope.success) {
		throw new ApiError(envelope.error ?? 'Unknown API error', res.status);
	}

	return envelope.data as T;
}

function get<T>(path: string, signal?: AbortSignal): Promise<T> {
	return request<T>(path, signal ? { signal } : undefined);
}

function post<T>(path: string, body?: unknown, signal?: AbortSignal): Promise<T> {
	const init: RequestInit = {
		method: 'POST',
		body: body !== undefined ? JSON.stringify(body) : undefined
	};
	if (signal) init.signal = signal;
	return request<T>(path, init);
}

function put<T>(path: string, body: unknown): Promise<T> {
	return request<T>(path, {
		method: 'PUT',
		body: JSON.stringify(body)
	});
}

function del<T>(path: string): Promise<T> {
	return request<T>(path, { method: 'DELETE' });
}

function patch<T>(path: string, body: unknown): Promise<T> {
	return request<T>(path, {
		method: 'PATCH',
		body: JSON.stringify(body)
	});
}

// ── Image inspect (new-app wizard pre-fill) ────────────────────────────
// `inspectImage` lets the new-app wizard pre-fill image port/healthcheck
// from a LOCAL image's metadata. It posts to the admin-gated discovery
// endpoint (the legacy POST /api/deploy/inspect route was dropped in the
// cutover). Local-only: it does not pull.

export function inspectImage(image: string, signal?: AbortSignal): Promise<InspectResult> {
	return post<InspectResult>('/api/discovery/image/inspect', { image }, signal);
}

// ── Discovery (/apps/new wizard helpers) ───────────────────────────
// These endpoints back the auto-discovery + connection-test flow that
// the static-site creation wizard used in the legacy /sites/new page.
// They are admin-gated; the token is plaintext over HTTPS and is not
// persisted server-side.

// GitProviderKind is the union the *frontend* sends. The empty string
// means "auto-detect server-side" (DetectProviderWithProbe runs).
export type GitProviderKind = '' | 'gitea' | 'github' | 'gitlab';

// DetectedGitProvider is the narrower union the backend's detect
// endpoint actually returns — `staticsite.DetectProviderWithProbe`
// always resolves to one of the three concrete kinds (it falls back to
// `gitea` for unknown hosts). Kept distinct from GitProviderKind so a
// successful detection cannot ever set the dropdown back to "".
export type DetectedGitProvider = 'gitea' | 'github' | 'gitlab';

export interface RepoInfo {
	owner: string;
	name: string;
	full_name: string;
	description: string;
	private: boolean;
	html_url: string;
}

export interface FolderEntry {
	path: string;
	is_dir: boolean;
}

export interface DiscoveryGitRequest {
	provider?: GitProviderKind;
	base_url: string;
	access_token?: string;
	repo_owner?: string;
	repo_name?: string;
	branch?: string;
	query?: string;
}

export interface ImageConflict {
	id: string;
	name: string;
	image: string;
	app_id?: string;
}

export function detectGitProvider(
	baseURL: string,
	signal?: AbortSignal
): Promise<{ provider: DetectedGitProvider }> {
	return post<{ provider: DetectedGitProvider }>(
		'/api/discovery/git/detect-provider',
		{ base_url: baseURL },
		signal
	);
}

export function testGitConnection(
	req: DiscoveryGitRequest,
	signal?: AbortSignal
): Promise<{ status: string }> {
	return post<{ status: string }>('/api/discovery/git/test-connection', req, signal);
}

export function listGitRepos(req: DiscoveryGitRequest, signal?: AbortSignal): Promise<RepoInfo[]> {
	return post<RepoInfo[]>('/api/discovery/git/repos', req, signal);
}

export function listGitBranches(
	req: DiscoveryGitRequest,
	signal?: AbortSignal
): Promise<string[]> {
	return post<string[]>('/api/discovery/git/branches', req, signal);
}

export function listGitTree(req: DiscoveryGitRequest, signal?: AbortSignal): Promise<FolderEntry[]> {
	return post<FolderEntry[]>('/api/discovery/git/tree', req, signal);
}

export function listImageConflicts(image: string, signal?: AbortSignal): Promise<ImageConflict[]> {
	return get<ImageConflict[]>(
		`/api/discovery/image/conflicts?image=${encodeURIComponent(image)}`,
		signal
	);
}

// ── Workload runtime view (runtime-state, storage, stop, start) ────
// Backed by internal/api/workload_runtime.go. The shapes mirror the
// Go side exactly so the UI can render without further normalization.

export interface WorkloadRuntimeState {
	source_kind: string;
	has_state: boolean;
	container_id?: string;
	state?: string;
	status?: string;
	last_commit_sha?: string;
	last_sync_at?: string;
	last_error?: string;
}

export interface WorkloadStorageUsage {
	source_kind: string;
	enabled: boolean;
	used_bytes: number;
	limit_mb?: number;
	probe_error?: string;
}

export interface StopStartResult {
	touched: number;
	failed: number;
}

export function getWorkloadRuntimeState(
	id: string,
	signal?: AbortSignal
): Promise<WorkloadRuntimeState> {
	return get<WorkloadRuntimeState>(`/api/workloads/${id}/runtime-state`, signal);
}

export function getWorkloadStorage(
	id: string,
	signal?: AbortSignal
): Promise<WorkloadStorageUsage> {
	return get<WorkloadStorageUsage>(`/api/workloads/${id}/storage`, signal);
}

export function stopWorkload(id: string): Promise<StopStartResult> {
	return post<StopStartResult>(`/api/workloads/${id}/stop`);
}

export function startWorkload(id: string): Promise<StopStartResult> {
	return post<StopStartResult>(`/api/workloads/${id}/start`);
}

// ── Registries ──────────────────────────────────────────────────────

export function listRegistries(): Promise<Registry[]> {
	return get<Registry[]>('/api/registries');
}

export function createRegistry(data: Partial<Registry>): Promise<Registry> {
	return post<Registry>('/api/registries', data);
}

export function updateRegistry(id: string, data: Partial<Registry>): Promise<Registry> {
	return put<Registry>(`/api/registries/${id}`, data);
}

export function deleteRegistry(id: string): Promise<{ deleted: string }> {
	return del<{ deleted: string }>(`/api/registries/${id}`);
}

export function testRegistry(id: string): Promise<{ status: string }> {
	return post<{ status: string }>(`/api/registries/${id}/test`);
}

export function listRegistryTags(registryId: string, image: string): Promise<string[]> {
	// Image contains slashes (e.g. "owner/name") — don't encode them;
	// the backend route uses a wildcard (/tags/*) that expects path segments.
	return get<string[]>(`/api/registries/${registryId}/tags/${image}`);
}

export function listRegistryImages(registryId: string): Promise<RegistryImage[]> {
	return get<RegistryImage[]>(`/api/registries/${registryId}/images`);
}

// ── Settings ────────────────────────────────────────────────────────

export function getSettings(signal?: AbortSignal): Promise<Settings> {
	return get<Settings>('/api/settings', signal);
}

export function updateSettings(data: Partial<Settings>): Promise<Settings> {
	return put<Settings>('/api/settings', data);
}

// ── Webhook envelopes ──────────────────────────────────────────────
// These shapes are reused by the workload + trigger webhook flows.

export interface WebhookUrlResponse {
	webhook_url: string;
	webhook_secret: string;
	has_signing_secret?: boolean;
	webhook_require_signature?: boolean;
}

export interface SigningSecretResponse {
	signing_secret: string;
}

export interface WebhookDelivery {
	id: number;
	target_type: 'project' | 'site' | 'workload' | 'trigger';
	target_id: string;
	target_name: string;
	received_at: string;
	source_ip: string;
	signature_state: 'valid' | 'invalid' | 'missing' | 'unconfigured';
	status_code: number;
	outcome: string;
	detail: string;
	body_size: number;
}

// ── Outgoing-webhook signing & test (settings tier only) ───────────
// Per-project, per-stage, per-site tiers were dropped with the legacy
// endpoints. Per-workload signing is exposed via the workload webhook
// flow below.

export interface NotificationSecretResponse {
	secret: string;
	has_secret: boolean;
}

export interface NotificationTestResult {
	url: string;
	tier: 'settings' | 'project' | 'stage' | 'site' | 'workload' | 'trigger';
	status_code: number;
	latency_ms: number;
	signature_sent: boolean;
	delivery_id: string;
	response_snippet: string;
	error?: string;
}

export function getSettingsNotificationSecret(): Promise<NotificationSecretResponse> {
	return get<NotificationSecretResponse>('/api/settings/notification-secret');
}
export function regenerateSettingsNotificationSecret(): Promise<NotificationSecretResponse> {
	return post<NotificationSecretResponse>('/api/settings/notification-secret/regenerate');
}
export function disableSettingsNotificationSigning(): Promise<NotificationSecretResponse> {
	return post<NotificationSecretResponse>('/api/settings/notification-secret/disable');
}
export function testSettingsNotification(): Promise<NotificationTestResult> {
	return post<NotificationTestResult>('/api/settings/notification-test');
}

// ── Proxy Routes ───────────────────────────────────────────────────

export function listProxyRoutes(signal?: AbortSignal): Promise<ProxyRoute[]> {
	return get<ProxyRoute[]>('/api/proxies', signal);
}

// ── Docker Management ──────────────────────────────────────────────

export function getUnusedImageStats(signal?: AbortSignal): Promise<{
	total_size_mb: number; count: number; threshold_mb: number; exceeded: boolean;
}> {
	return get<{ total_size_mb: number; count: number; threshold_mb: number; exceeded: boolean }>('/api/docker/unused-images', signal);
}

export function pruneImages(): Promise<{ images_removed: number; space_reclaimed_mb: number }> {
	return post<{ images_removed: number; space_reclaimed_mb: number }>('/api/docker/prune-images');
}

export function pruneBuildCache(): Promise<{ caches_deleted: number; space_reclaimed_mb: number }> {
	return post<{ caches_deleted: number; space_reclaimed_mb: number }>('/api/docker/prune-build-cache');
}

export function testNpmConnection(data: { npm_url?: string; npm_email?: string; npm_password?: string }): Promise<{ status: string }> {
	return post<{ status: string }>('/api/settings/npm/test', data);
}

// ── NPM friendly-name cache ─────────────────────────────────────────
// The settings/NPM page first renders "Certificate #<id>" / "Access List
// #<id>" then swaps to the friendly name once the list resolves — a visible
// flicker on every tab re-entry and after a reload. Back the two list calls
// with a short-lived sessionStorage cache so names resolve instantly within
// a session (survives reloads, scoped to the tab). Pass `force` to bypass it
// — the picker's "browse" action wants fresh data.
const NPM_CACHE_TTL_MS = 5 * 60 * 1000;

interface NpmCacheEntry<T> {
	ts: number;
	data: T;
}

function readNpmCache<T>(key: string): T | null {
	if (typeof sessionStorage === 'undefined') return null;
	try {
		const raw = sessionStorage.getItem(key);
		if (!raw) return null;
		const entry = JSON.parse(raw) as NpmCacheEntry<T>;
		if (!entry || typeof entry.ts !== 'number') return null;
		if (Date.now() - entry.ts > NPM_CACHE_TTL_MS) return null;
		return entry.data;
	} catch {
		// Corrupt/unparseable cache — treat as a miss.
		return null;
	}
}

function writeNpmCache<T>(key: string, data: T): void {
	if (typeof sessionStorage === 'undefined') return;
	try {
		const entry: NpmCacheEntry<T> = { ts: Date.now(), data };
		sessionStorage.setItem(key, JSON.stringify(entry));
	} catch {
		// Quota/serialization failure is non-fatal — the call still returned
		// fresh data; we just don't get the cache speedup next time.
	}
}

const NPM_CERTS_CACHE_KEY = 'dw_npm_certs';
const NPM_ACCESS_LISTS_CACHE_KEY = 'dw_npm_access_lists';

/**
 * List NPM SSL certificates. Cached in sessionStorage for ~5 minutes so the
 * settings page resolves friendly names without a flicker on re-entry/reload.
 * Pass `force` to skip the cache and refresh (e.g. opening the picker).
 */
export async function listNpmCertificates(force = false): Promise<NpmCertificate[]> {
	if (!force) {
		const cached = readNpmCache<NpmCertificate[]>(NPM_CERTS_CACHE_KEY);
		if (cached) return cached;
	}
	const data = await get<NpmCertificate[]>('/api/settings/npm-certificates');
	writeNpmCache(NPM_CERTS_CACHE_KEY, data);
	return data;
}

/**
 * List NPM access lists. Cached in sessionStorage for ~5 minutes (see
 * {@link listNpmCertificates}). Pass `force` to skip the cache and refresh.
 */
export async function listNpmAccessLists(force = false): Promise<NpmAccessList[]> {
	if (!force) {
		const cached = readNpmCache<NpmAccessList[]>(NPM_ACCESS_LISTS_CACHE_KEY);
		if (cached) return cached;
	}
	const data = await get<NpmAccessList[]>('/api/settings/npm-access-lists');
	writeNpmCache(NPM_ACCESS_LISTS_CACHE_KEY, data);
	return data;
}

// ── Volume scopes (metadata only) ───────────────────────────────────
// Per-project volume CRUD endpoints died with the legacy routes; the
// workload volume endpoints cover the new path. Scope metadata stays
// because the volume editor for workloads still needs it.

export function listVolumeScopes(): Promise<VolumeScopeInfo[]> {
	return get<VolumeScopeInfo[]>('/api/volumes/scopes');
}

// ── DNS ────────────────────────────────────────────────────────────

export function testDnsConnection(provider: string, token: string, zoneId: string): Promise<{ success: boolean; error?: string }> {
	return post<{ success: boolean; error?: string }>('/api/settings/dns/test', { provider, token, zone_id: zoneId });
}

export function listDnsZones(token?: string): Promise<DnsZone[]> {
	return post<DnsZone[]>('/api/settings/dns/zones', { token: token ?? '' });
}

export function getDnsRecords(): Promise<DnsRecordView[]> {
	return get<DnsRecordView[]>('/api/dns/records');
}

export function syncDnsRecords(): Promise<{ created: number; deleted: number; already_synced: number }> {
	return post<{ created: number; deleted: number; already_synced: number }>('/api/dns/sync');
}

export function deleteDnsRecord(fqdn: string): Promise<void> {
	return del<void>(`/api/dns/records/${encodeURIComponent(fqdn)}`);
}

// ── Backups ────────────────────────────────────────────────────────

export function listBackups(): Promise<BackupInfo[]> {
	return get<BackupInfo[]>('/api/backups');
}

export function triggerBackup(): Promise<BackupInfo> {
	return post<BackupInfo>('/api/backups');
}

export function deleteBackup(id: string): Promise<void> {
	return del<void>(`/api/backups/${id}`);
}

export function restoreBackup(id: string): Promise<{ status: string; message: string }> {
	// X-Confirm-Restore echoes the backup id. The backend rejects any
	// POST whose header doesn't match the path param — this defeats
	// blind CSRF (a cross-origin form/image-tag POST can't set custom
	// headers without a preflight). Sent alongside the bearer JWT.
	return request<{ status: string; message: string }>(`/api/backups/${id}/restore`, {
		method: 'POST',
		headers: { 'X-Confirm-Restore': id }
	});
}

export function backupDownloadUrl(id: string): string {
	return `/api/backups/${id}/download`;
}

// ── Volume Snapshots ───────────────────────────────────────────────
// Per-workload archives of host-bind data volumes. Capture-only for now
// (create/list/delete/download); restore is a separate later phase.

export interface SnapshotInfo {
	id: string;
	workload_id: string;
	label: string;
	filename: string;
	size_bytes: number;
	manifest: string; // JSON-encoded [{ index, target, scope, source }]
	created_at: string;
}

export interface SnapshotableVolume {
	target: string;
	scope: string;
	source: string;
}

export interface SkippedVolume {
	target: string;
	scope: string;
	reason: string;
}

export interface SnapshotableInfo {
	volumes: SnapshotableVolume[];
	skipped: SkippedVolume[];
}

export function listWorkloadSnapshots(workloadId: string, signal?: AbortSignal): Promise<SnapshotInfo[]> {
	return get<SnapshotInfo[]>(`/api/workloads/${workloadId}/snapshots`, signal);
}

export function getWorkloadSnapshotable(workloadId: string, signal?: AbortSignal): Promise<SnapshotableInfo> {
	return get<SnapshotableInfo>(`/api/workloads/${workloadId}/snapshotable`, signal);
}

export function createWorkloadSnapshot(workloadId: string, label?: string): Promise<SnapshotInfo> {
	return post<SnapshotInfo>(`/api/workloads/${workloadId}/snapshots`, label ? { label } : {});
}

export function deleteSnapshot(sid: string): Promise<void> {
	return del<void>(`/api/snapshots/${sid}`);
}

export function snapshotDownloadUrl(sid: string): string {
	return `/api/snapshots/${sid}/download`;
}

// ── Health ──────────────────────────────────────────────────────────

export function getHealth(): Promise<{ docker: DockerHealth; proxy?: ProxyHealth }> {
	return get<{ docker: DockerHealth; proxy?: ProxyHealth }>('/api/health');
}

// ── Auth ─────────────────────────────────────────────────────────────

export function login(username: string, password: string): Promise<{ token: string; expires_at: string }> {
	return post<{ token: string; expires_at: string }>('/api/auth/login', { username, password });
}

export function getCurrentUser(): Promise<{ id: string; username: string; email: string; role: string }> {
	return get<{ id: string; username: string; email: string; role: string }>('/api/auth/me');
}

// ── Auth settings & user management ─────────────────────────────────
// Previously typed as `any`, which silently disabled type checking on
// the entire user/auth surface — including the password-change call.
// All routes are admin-gated server-side.

export interface AuthSettings {
	auth_mode: 'local' | 'oidc';
	oidc_client_id: string;
	oidc_client_secret: string;
	oidc_issuer_url: string;
	oidc_redirect_url: string;
}

export interface AuthUser {
	id: string;
	username: string;
	email: string;
	role: 'admin' | 'viewer' | string;
	created_at: string;
}

export interface CreateUserInput {
	username: string;
	password: string;
	email?: string;
	role?: 'admin' | 'viewer' | string;
}

export interface UpdateUserInput {
	email?: string;
	role?: 'admin' | 'viewer' | string;
}

export async function getAuthSettings(): Promise<AuthSettings> {
	return request<AuthSettings>('/api/auth/settings');
}

export async function updateAuthSettings(settings: AuthSettings): Promise<AuthSettings> {
	return request<AuthSettings>('/api/auth/settings', {
		method: 'PUT',
		body: JSON.stringify(settings)
	});
}

export async function listUsers(): Promise<AuthUser[]> {
	return request<AuthUser[]>('/api/auth/users');
}

export async function createUser(data: CreateUserInput): Promise<AuthUser> {
	return request<AuthUser>('/api/auth/users', {
		method: 'POST',
		body: JSON.stringify(data)
	});
}

export async function updateUser(uid: string, data: UpdateUserInput): Promise<AuthUser> {
	return request<AuthUser>(`/api/auth/users/${uid}`, {
		method: 'PUT',
		body: JSON.stringify(data)
	});
}

export async function changeUserPassword(uid: string, password: string): Promise<void> {
	await request<void>(`/api/auth/users/${uid}/password`, {
		method: 'PUT',
		body: JSON.stringify({ password })
	});
}

export async function deleteUser(uid: string): Promise<void> {
	await request<void>(`/api/auth/users/${uid}`, { method: 'DELETE' });
}

export async function logout(): Promise<void> {
	await request<void>('/api/auth/logout', { method: 'POST' });
}

// ── Config Export ────────────────────────────────────────────────────

export function exportConfigUrl(): string {
	return '/api/config/export';
}

// ── Workload volume browse / download / upload ─────────────────────
// The browse/download/upload helpers now target /api/workloads/{id}
// instead of the deleted project-scoped path. Source/path/scope params
// retain the same query keys for compatibility with the volume editor.

export function browseVolume(
	workloadId: string,
	volId: string,
	params?: { path?: string; reference?: string }
): Promise<BrowseResult> {
	const query = new URLSearchParams();
	if (params?.path) query.set('path', params.path);
	if (params?.reference) query.set('reference', params.reference);
	const qs = query.toString();
	return get<BrowseResult>(
		`/api/workloads/${workloadId}/volumes/${volId}/browse${qs ? `?${qs}` : ''}`
	);
}

export function volumeDownloadUrl(
	workloadId: string,
	volId: string,
	params?: { path?: string; reference?: string }
): string {
	const query = new URLSearchParams();
	if (params?.path) query.set('path', params.path);
	if (params?.reference) query.set('reference', params.reference);
	const token = typeof localStorage !== 'undefined' ? localStorage.getItem('auth_token') : null;
	if (token) query.set('token', token);
	const qs = query.toString();
	return `/api/workloads/${workloadId}/volumes/${volId}/download${qs ? `?${qs}` : ''}`;
}

export async function uploadToVolume(
	workloadId: string,
	volId: string,
	files: FileList,
	params?: { path?: string; reference?: string }
): Promise<{ uploaded: string[]; count: number }> {
	const query = new URLSearchParams();
	if (params?.path) query.set('path', params.path);
	if (params?.reference) query.set('reference', params.reference);
	const qs = query.toString();

	const formData = new FormData();
	for (let i = 0; i < files.length; i++) {
		formData.append('files', files[i]);
	}

	const token = typeof localStorage !== 'undefined' ? localStorage.getItem('auth_token') : null;
	const headers: Record<string, string> = {};
	if (token) headers['Authorization'] = `Bearer ${token}`;

	const res = await fetch(
		`/api/workloads/${workloadId}/volumes/${volId}/upload${qs ? `?${qs}` : ''}`,
		{
			method: 'POST',
			headers,
			body: formData
		}
	);

	const envelope = await res.json();
	if (!envelope.success) throw new Error(envelope.error ?? 'Upload failed');
	return envelope.data;
}

// ── Event Log ───────────────────────────────────────────────────────

export function fetchEventLog(params?: {
	severity?: string;
	source?: string;
	since?: string;
	until?: string;
	limit?: number;
	offset?: number;
}): Promise<EventLogEntry[]> {
	const query = new URLSearchParams();
	if (params?.severity) query.set('severity', params.severity);
	if (params?.source) query.set('source', params.source);
	if (params?.since) query.set('since', params.since);
	if (params?.until) query.set('until', params.until);
	if (params?.limit) query.set('limit', String(params.limit));
	if (params?.offset) query.set('offset', String(params.offset));
	const qs = query.toString();
	return get<EventLogEntry[]>(`/api/events/log${qs ? `?${qs}` : ''}`);
}

export function fetchEventLogStats(signal?: AbortSignal): Promise<EventLogStats> {
	return get<EventLogStats>('/api/events/log/stats', signal);
}

export function fetchWorkloadEvents(
	id: string,
	params?: { severity?: string; limit?: number; offset?: number },
	signal?: AbortSignal
): Promise<EventLogEntry[]> {
	const query = new URLSearchParams();
	if (params?.severity) query.set('severity', params.severity);
	if (params?.limit) query.set('limit', String(params.limit));
	if (params?.offset) query.set('offset', String(params.offset));
	const qs = query.toString();
	return get<EventLogEntry[]>(`/api/workloads/${id}/events${qs ? `?${qs}` : ''}`, signal);
}

export function deleteEvent(id: number): Promise<{ status: string }> {
	return del<{ status: string }>(`/api/events/log/${id}`);
}

export function clearAllEvents(): Promise<{ status: string; count: number }> {
	return del<{ status: string; count: number }>('/api/events/log');
}

// ── Stale Containers ────────────────────────────────────────────────

export function fetchStaleContainers(signal?: AbortSignal): Promise<StaleContainer[]> {
	return get<StaleContainer[]>('/api/containers/stale', signal);
}

export function cleanupStaleContainer(id: string): Promise<{ deleted: string }> {
	return post<{ deleted: string }>(`/api/containers/stale/${id}/cleanup`);
}

export function bulkCleanupStaleContainers(): Promise<{ deleted: number }> {
	return post<{ deleted: number }>('/api/containers/stale/cleanup');
}

// ── System Stats ───────────────────────────────────────────────────

export function fetchSystemStats(signal?: AbortSignal): Promise<SystemStats> {
	return get<SystemStats>('/api/system/stats', signal);
}

export function fetchSystemStatsHistory(
	window = '2h',
	signal?: AbortSignal
): Promise<SystemStatsSample[]> {
	return get<SystemStatsSample[]>(`/api/system/stats/history?window=${encodeURIComponent(window)}`, signal);
}

export function fetchTopContainers(
	by: 'cpu' | 'memory' = 'cpu',
	limit = 5,
	signal?: AbortSignal
): Promise<TopContainerSample[]> {
	return get<TopContainerSample[]>(`/api/system/stats/top?by=${by}&limit=${limit}`, signal);
}

// ── Per-container stats (workload-scoped) ──────────────────────────
// Project / stage / instance + static-site stats endpoints died with
// the legacy routes. Use the workload-scoped endpoint for any per-
// container CPU/memory drill-down.

export function fetchWorkloadContainerStats(
	workloadId: string,
	containerRowId: string,
	signal?: AbortSignal
): Promise<ContainerStats> {
	return get<ContainerStats>(
		`/api/workloads/${workloadId}/containers/${containerRowId}/stats`,
		signal
	);
}

export function fetchWorkloadContainerStatsHistory(
	workloadId: string,
	containerRowId: string,
	window = '2h',
	signal?: AbortSignal
): Promise<ContainerStatsSample[]> {
	return get<ContainerStatsSample[]>(
		`/api/workloads/${workloadId}/containers/${containerRowId}/stats/history?window=${encodeURIComponent(window)}`,
		signal
	);
}

// ── Workloads ───────────────────────────────────────────────────────

export function listWorkloads(kind?: WorkloadKind, signal?: AbortSignal): Promise<Workload[]> {
	const path = kind ? `/api/workloads?kind=${encodeURIComponent(kind)}` : '/api/workloads';
	return get<Workload[]>(path, signal);
}

export function getWorkload(id: string, signal?: AbortSignal): Promise<Workload> {
	return get<Workload>(`/api/workloads/${id}`, signal);
}

export function listWorkloadContainers(id: string, signal?: AbortSignal): Promise<Container[]> {
	return get<Container[]>(`/api/workloads/${id}/containers`, signal);
}

export function setWorkloadAppID(id: string, appID: string): Promise<Workload> {
	return patch<Workload>(`/api/workloads/${id}/app`, { app_id: appID });
}

export function createPluginWorkload(body: import('./types').PluginWorkloadInput): Promise<Workload> {
	return post<Workload>('/api/workloads', body);
}

export function updatePluginWorkload(id: string, body: import('./types').PluginWorkloadInput): Promise<Workload> {
	return put<Workload>(`/api/workloads/${id}/plugin`, body);
}

export function deployPluginWorkload(
	id: string,
	body?: { reference?: string; note?: string }
): Promise<{ workload_id: string; reference: string; triggered_by: string }> {
	return post(`/api/workloads/${id}/deploy`, body ?? {});
}

// ── Deploy history + rollback ───────────────────────────────────────
// Structured, version-pinned ledger of every deploy dispatch (success and
// failure). `rollbackable` is computed server-side: a successful deploy of a
// source kind that supports reference-pinned redeploy (image today).
export interface DeployHistoryEntry {
	id: number;
	workload_id: string;
	source_kind: string;
	reference: string;
	reason: string;
	triggered_by: string;
	note: string;
	outcome: 'success' | 'failure';
	error: string;
	started_at: string;
	finished_at: string;
	rollbackable: boolean;
}

export function fetchWorkloadDeploys(
	id: string,
	params?: { limit?: number; offset?: number },
	signal?: AbortSignal
): Promise<DeployHistoryEntry[]> {
	const query = new URLSearchParams();
	if (params?.limit) query.set('limit', String(params.limit));
	if (params?.offset) query.set('offset', String(params.offset));
	const qs = query.toString();
	return get<DeployHistoryEntry[]>(`/api/workloads/${id}/deploys${qs ? `?${qs}` : ''}`, signal);
}

export function rollbackWorkload(
	id: string,
	deployId: number
): Promise<{ workload_id: string; reference: string; rollback_of: number; triggered_by: string }> {
	return post(`/api/workloads/${id}/rollback`, { deploy_id: deployId });
}

// ── GitOps (config-as-code) ─────────────────────────────────────────
// One rich payload per workload folds the file preview, parsed status, and
// field-level drift into a single GET so the panel makes one call. The shape
// mirrors the Go `gitOpsStatusResponse` (snake_case is preserved end-to-end,
// matching the rest of this file). Drift entries list only the declared
// fields that DIFFER from live; `managed_fields` lists every key the file
// declares (the read-only gate keys on these).
export interface GitOpsDriftEntry {
	field: string;
	repo_value: string;
	live_value: string;
}

export type GitOpsStatusKind = 'disabled' | 'ok' | 'no_file' | 'fetch_failed' | 'invalid';

export interface GitOpsStatus {
	eligible: boolean;
	enabled: boolean;
	path: string;
	status: GitOpsStatusKind;
	raw: string;
	message: string;
	commit_sha: string;
	last_sync_at: string;
	drift: GitOpsDriftEntry[];
	drift_count: number;
	managed_fields: string[];
}

export function fetchWorkloadGitOps(id: string, signal?: AbortSignal): Promise<GitOpsStatus> {
	return get<GitOpsStatus>(`/api/workloads/${id}/gitops`, signal);
}

export function setWorkloadGitOps(
	id: string,
	body: { enabled: boolean; path: string }
): Promise<{ enabled: boolean; path: string }> {
	return put<{ enabled: boolean; path: string }>(`/api/workloads/${id}/gitops`, body);
}

export function syncWorkloadGitOps(
	id: string
): Promise<{ status: string; commit_sha: string; applied_fields: string[]; triggered_by: string }> {
	return post(`/api/workloads/${id}/gitops/sync`);
}

// ── Per-workload metrics history ────────────────────────────────────
// CPU% and memory (bytes) summed across the workload's containers, one
// point per sampled timestamp. Empty when stats collection is off / Docker
// was down / the workload is new.
export interface WorkloadStatsPoint {
	ts: number;
	cpu_percent: number;
	memory_usage: number;
	memory_limit: number;
}

export function fetchWorkloadStatsHistory(
	id: string,
	window = '2h',
	signal?: AbortSignal
): Promise<WorkloadStatsPoint[]> {
	return get<WorkloadStatsPoint[]>(
		`/api/workloads/${id}/stats/history?window=${encodeURIComponent(window)}`,
		signal
	);
}

export function listHookKinds(signal?: AbortSignal): Promise<import('./types').HookKinds> {
	return get<import('./types').HookKinds>('/api/hooks/kinds', signal);
}

export function deletePluginWorkload(id: string): Promise<{ deleted: string }> {
	return del<{ deleted: string }>(`/api/workloads/${id}`);
}

export interface WorkloadEnv {
	id: string;
	workload_id: string;
	key: string;
	value: string;
	encrypted: boolean;
	created_at: string;
	updated_at: string;
}

export function listWorkloadEnv(id: string, signal?: AbortSignal): Promise<WorkloadEnv[]> {
	return get<WorkloadEnv[]>(`/api/workloads/${id}/env`, signal);
}

export function setWorkloadEnv(
	id: string,
	body: { key: string; value: string; encrypted: boolean }
): Promise<WorkloadEnv> {
	return put<WorkloadEnv>(`/api/workloads/${id}/env`, body);
}

export function deleteWorkloadEnv(id: string, envID: string): Promise<{ deleted: string }> {
	return del<{ deleted: string }>(`/api/workloads/${id}/env/${envID}`);
}

// Workload-level webhook URL accessors were removed in the hard legacy
// cutover: inbound webhooks are now first-class Triggers. To wire a
// workload to inbound deploys, create or bind a Trigger via the
// /triggers UI (which mints a /api/webhook/triggers/{secret} URL).

// Per-workload outbound notification routes (Slack/Discord/etc).
// Multi-destination fan-out — the dispatcher sends to every enabled
// row whose event_types allow-list matches the event. An empty
// event_types means "match every event". Secret round-trips as
// write-only: the API returns secret_set, never the ciphertext.
export interface WorkloadNotification {
	id: string;
	workload_id: string;
	name: string;
	url: string;
	secret_set: boolean;
	event_types: string;
	enabled: boolean;
	sort_order: number;
	created_at: string;
	updated_at: string;
}

export interface WorkloadNotificationInput {
	name: string;
	url: string;
	secret?: string;
	event_types?: string;
	enabled?: boolean;
	sort_order?: number;
}

export function listWorkloadNotifications(
	id: string,
	signal?: AbortSignal
): Promise<WorkloadNotification[]> {
	return get<WorkloadNotification[]>(`/api/workloads/${id}/notifications`, signal);
}

export function createWorkloadNotification(
	id: string,
	body: WorkloadNotificationInput
): Promise<WorkloadNotification> {
	return post<WorkloadNotification>(`/api/workloads/${id}/notifications`, body);
}

export function updateWorkloadNotification(
	id: string,
	nid: string,
	body: WorkloadNotificationInput
): Promise<WorkloadNotification> {
	return put<WorkloadNotification>(`/api/workloads/${id}/notifications/${nid}`, body);
}

export function deleteWorkloadNotification(
	id: string,
	nid: string
): Promise<{ success: boolean }> {
	return del<{ success: boolean }>(`/api/workloads/${id}/notifications/${nid}`);
}

export function fetchWorkloadContainerLogs(
	workloadId: string,
	containerRowId: string,
	tail: number
): Promise<string[]> {
	return get<string[]>(
		`/api/workloads/${workloadId}/containers/${containerRowId}/logs?tail=${tail}`
	);
}

export interface WorkloadVolume {
	id: string;
	workload_id: string;
	source: string;
	target: string;
	scope: string;
	name: string;
	created_at: string;
	updated_at: string;
}

export function listWorkloadVolumes(id: string, signal?: AbortSignal): Promise<WorkloadVolume[]> {
	return get<WorkloadVolume[]>(`/api/workloads/${id}/volumes`, signal);
}

export function setWorkloadVolume(
	id: string,
	body: { source: string; target: string; scope: string; name?: string }
): Promise<WorkloadVolume> {
	return put<WorkloadVolume>(`/api/workloads/${id}/volumes`, body);
}

export function deleteWorkloadVolume(id: string, volID: string): Promise<{ deleted: string }> {
	return del<{ deleted: string }>(`/api/workloads/${id}/volumes/${volID}`);
}

export interface HookKindSchema {
	kind: string;
	sample: unknown;
}

export function getHookKindSchema(kind: string, signal?: AbortSignal): Promise<HookKindSchema> {
	return get<HookKindSchema>(`/api/hooks/kinds/${kind}/schema`, signal);
}

// ── Triggers (first-class redeploy signal sources) ──────────────────

export interface RedeployTrigger {
	id: string;
	kind: string;
	name: string;
	config: unknown;
	webhook_enabled: boolean;
	webhook_require_signature: boolean;
	binding_count: number;
	/** RFC3339 timestamp the scheduler last dispatched this trigger. Empty for
	 *  never-fired or non-scheduler-driven triggers. */
	last_fired_at: string;
	created_at: string;
	updated_at: string;
}

export interface TriggerWebhook {
	url: string;
	secret: string;
	webhook_require_signature: boolean;
}

export interface TriggerBinding {
	id: string;
	workload_id: string;
	workload_name: string;
	trigger_id: string;
	binding_config: unknown;
	enabled: boolean;
	sort_order: number;
	created_at: string;
	updated_at: string;
}

export interface WorkloadTriggerBinding extends TriggerBinding {
	trigger_kind: string;
	trigger_name: string;
}

export interface TriggerInput {
	kind: string;
	name: string;
	config: unknown;
	webhook_enabled: boolean;
	webhook_require_signature: boolean;
}

export interface BindingInput {
	workload_id: string;
	binding_config?: unknown;
	enabled?: boolean;
	sort_order?: number;
}

export interface WorkloadBindInput {
	trigger_id?: string;
	binding_config?: unknown;
	enabled?: boolean;
	sort_order?: number;
	inline?: TriggerInput;
}

export function listTriggers(kind?: string, signal?: AbortSignal): Promise<RedeployTrigger[]> {
	const path = kind ? `/api/triggers?kind=${encodeURIComponent(kind)}` : '/api/triggers';
	return get<RedeployTrigger[]>(path, signal);
}

export function getTrigger(id: string, signal?: AbortSignal): Promise<RedeployTrigger> {
	return get<RedeployTrigger>(`/api/triggers/${id}`, signal);
}

export function createTrigger(body: TriggerInput): Promise<RedeployTrigger> {
	return post<RedeployTrigger>('/api/triggers', body);
}

export function updateTrigger(id: string, body: TriggerInput): Promise<RedeployTrigger> {
	return put<RedeployTrigger>(`/api/triggers/${id}`, body);
}

export function deleteTrigger(id: string): Promise<{ deleted: string }> {
	return del<{ deleted: string }>(`/api/triggers/${id}`);
}

export function getTriggerWebhook(id: string, signal?: AbortSignal): Promise<TriggerWebhook> {
	return get<TriggerWebhook>(`/api/triggers/${id}/webhook`, signal);
}

export function regenerateTriggerWebhook(id: string): Promise<{ secret: string; url: string }> {
	return post<{ secret: string; url: string }>(`/api/triggers/${id}/webhook/regenerate`);
}

export interface FireNowResponse {
	trigger: string;
	fired_at: string;
	bindings: number;
	deployed: number;
	errored: number;
}

/** Fire a schedule trigger immediately without waiting for the next
 *  natural fire window. Backend rejects with 400 for non-schedule kinds. */
export function fireTriggerNow(id: string): Promise<FireNowResponse> {
	return post<FireNowResponse>(`/api/triggers/${id}/fire`);
}

export function listBindingsForTrigger(id: string, signal?: AbortSignal): Promise<TriggerBinding[]> {
	return get<TriggerBinding[]>(`/api/triggers/${id}/bindings`, signal);
}

export function bindWorkloadToTrigger(triggerId: string, body: BindingInput): Promise<TriggerBinding> {
	return post<TriggerBinding>(`/api/triggers/${triggerId}/bindings`, body);
}

export function listBindingsForWorkload(
	workloadId: string,
	signal?: AbortSignal
): Promise<WorkloadTriggerBinding[]> {
	return get<WorkloadTriggerBinding[]>(`/api/workloads/${workloadId}/triggers`, signal);
}

export function bindTriggerToWorkload(
	workloadId: string,
	body: WorkloadBindInput
): Promise<TriggerBinding> {
	return post<TriggerBinding>(`/api/workloads/${workloadId}/triggers`, body);
}

export function updateBinding(
	id: string,
	body: { binding_config?: unknown; enabled?: boolean; sort_order?: number }
): Promise<TriggerBinding> {
	return put<TriggerBinding>(`/api/bindings/${id}`, body);
}

export function deleteBinding(id: string): Promise<{ deleted: string }> {
	return del<{ deleted: string }>(`/api/bindings/${id}`);
}

export interface WorkloadChainNode {
	id: string;
	name: string;
	source_kind: string;
	trigger_kind: string;
	/** True when this child was materialized as a branch preview of the chain's
	 *  `self` workload (vs. an operator-created stage child). Always false for
	 *  the parent and self nodes. */
	is_preview?: boolean;
	/** The git branch a preview child was deployed for. Empty for non-preview
	 *  nodes (omitted by the server). */
	preview_branch?: string;
	created_at: string;
	updated_at: string;
}

export interface WorkloadChain {
	parent: WorkloadChainNode | null;
	self: WorkloadChainNode;
	children: WorkloadChainNode[];
}

export function getWorkloadChain(id: string, signal?: AbortSignal): Promise<WorkloadChain> {
	return get<WorkloadChain>(`/api/workloads/${id}/chain`, signal);
}

export function promoteFromWorkload(
	targetID: string,
	sourceID: string,
	body?: { image_tag?: string; deploy?: boolean }
): Promise<{ workload_id: string; source_id: string; promoted_tag: string; deploy_queued: boolean }> {
	return post(`/api/workloads/${targetID}/promote-from/${sourceID}`, body ?? {});
}

// ── Containers (global index) ───────────────────────────────────────

export interface ListContainersFilter {
	workload_id?: string;
	kind?: WorkloadKind;
	state?: string;
	app_id?: string;
}

export function listContainers(filter: ListContainersFilter = {}, signal?: AbortSignal): Promise<ContainerView[]> {
	const params = new URLSearchParams();
	for (const [k, v] of Object.entries(filter)) {
		// Skip unset / empty filters; explicitly check undefined and '' instead
		// of truthy so future filter shapes (numbers, booleans) aren't dropped.
		if (v !== undefined && v !== '') params.set(k, String(v));
	}
	const qs = params.toString();
	const path = qs ? `/api/containers?${qs}` : '/api/containers';
	return get<ContainerView[]>(path, signal);
}

// ── Apps ────────────────────────────────────────────────────────────

export function listApps(signal?: AbortSignal): Promise<App[]> {
	return get<App[]>('/api/apps', signal);
}

export function getApp(id: string, signal?: AbortSignal): Promise<App> {
	return get<App>(`/api/apps/${id}`, signal);
}

export function createApp(data: { name: string; description?: string }): Promise<App> {
	return post<App>('/api/apps', data);
}

export function updateApp(id: string, data: { name: string; description?: string }): Promise<App> {
	return put<App>(`/api/apps/${id}`, data);
}

export function deleteApp(id: string): Promise<void> {
	return del<void>(`/api/apps/${id}`);
}

// ── Event Triggers ──────────────────────────────────────────────────
// Backend: internal/api/event_triggers.go. AND-composed filter shape;
// empty filter fields mean "match any value." The dispatcher fans
// matching event_log entries out to action_target via signed webhook.

export interface EventTrigger {
	id: number;
	name: string;
	filter_severity: string; // CSV; "" = any
	filter_source: string;   // CSV; "" = any
	filter_message_regex: string; // "" = any
	action_type: string;          // 'webhook' today
	action_target: string;        // URL
	action_secret: string;        // optional HMAC secret
	enabled: boolean;
	created_at: string;
	updated_at: string;
}

export interface EventTriggerInput {
	name: string;
	filter_severity?: string;
	filter_source?: string;
	filter_message_regex?: string;
	action_type?: string;
	action_target: string;
	action_secret?: string;
	enabled?: boolean;
}

export function listEventTriggers(signal?: AbortSignal): Promise<EventTrigger[]> {
	return get<EventTrigger[]>('/api/event-triggers', signal);
}

export function getEventTrigger(id: number, signal?: AbortSignal): Promise<EventTrigger> {
	return get<EventTrigger>(`/api/event-triggers/${id}`, signal);
}

export function createEventTrigger(data: EventTriggerInput): Promise<EventTrigger> {
	return post<EventTrigger>('/api/event-triggers', data);
}

export function updateEventTrigger(id: number, data: EventTriggerInput): Promise<EventTrigger> {
	return patch<EventTrigger>(`/api/event-triggers/${id}`, data);
}

export function deleteEventTrigger(id: number): Promise<void> {
	return del<void>(`/api/event-triggers/${id}`);
}

export function testEventTrigger(id: number): Promise<NotificationTestResult> {
	return post<NotificationTestResult>(`/api/event-triggers/${id}/test`);
}

// ── Log scan rules ──────────────────────────────────────────────────
// Backend: internal/api/log_scan_rules.go. Rules are regex patterns
// the scanner manager evaluates against container log lines. Scope
// model: workload_id="" + overrides_id=0 → global; workload_id set →
// workload-only (or per-workload override of a global via
// overrides_id).

export interface LogScanRule {
	id: number;
	workload_id: string; // "" = global
	overrides_id: number; // 0 = not an override
	name: string;
	pattern: string;
	severity: 'info' | 'warn' | 'error';
	streams: 'all' | 'stdout' | 'stderr';
	cooldown_seconds: number;
	enabled: boolean;
	created_at: string;
	updated_at: string;
}

export interface LogScanRuleInput {
	workload_id?: string;
	overrides_id?: number;
	name: string;
	pattern: string;
	severity?: 'info' | 'warn' | 'error';
	streams?: 'all' | 'stdout' | 'stderr';
	cooldown_seconds?: number;
	enabled?: boolean;
}

export interface LogScanTestResult {
	matched: boolean;
	captures?: Record<string, string>;
	error?: string;
}

export function listLogScanRules(opts?: {
	workloadID?: string;
	signal?: AbortSignal;
}): Promise<LogScanRule[]> {
	const params = opts?.workloadID ? `?workload_id=${encodeURIComponent(opts.workloadID)}` : '';
	return get<LogScanRule[]>(`/api/log-scan-rules${params}`, opts?.signal);
}

export function getLogScanRule(id: number, signal?: AbortSignal): Promise<LogScanRule> {
	return get<LogScanRule>(`/api/log-scan-rules/${id}`, signal);
}

export function createLogScanRule(data: LogScanRuleInput): Promise<LogScanRule> {
	return post<LogScanRule>('/api/log-scan-rules', data);
}

export function updateLogScanRule(id: number, data: LogScanRuleInput): Promise<LogScanRule> {
	return patch<LogScanRule>(`/api/log-scan-rules/${id}`, data);
}

export function deleteLogScanRule(id: number): Promise<void> {
	return del<void>(`/api/log-scan-rules/${id}`);
}

export function testLogScanRule(id: number, sampleLine: string): Promise<LogScanTestResult> {
	return post<LogScanTestResult>(`/api/log-scan-rules/${id}/test`, { sample_line: sampleLine });
}

export function getEffectiveLogScanRules(workloadID: string, signal?: AbortSignal): Promise<LogScanRule[]> {
	return get<LogScanRule[]>(`/api/workloads/${workloadID}/effective-rules`, signal);
}

export interface LogScanStats {
	engine: {
		dropped_by_bucket: number;
		dropped_by_cooldown: number;
	};
	active_tails: number;
	last_compile_errors: string[];
}

export function getLogScanStats(signal?: AbortSignal): Promise<LogScanStats> {
	return get<LogScanStats>('/api/log-scan-rules/stats', signal);
}

// ── Metric alert rules ──────────────────────────────────────────────
// Backend: internal/api/metric_alert_rules.go. Rules compare a sampled
// container metric (cpu/memory) against a threshold using a comparator.
// Scope model: workload_id="" → global; workload_id set → workload-only.
// Unlike log-scan rules there is no override / test / effective-rules
// concept — a metric-alert rule is a flat threshold check.

export interface MetricAlertRule {
	id: number;
	workload_id: string; // "" = global
	name: string;
	metric: 'cpu_percent' | 'memory_percent' | 'memory_bytes';
	comparator: 'gt' | 'lt';
	threshold: number;
	severity: 'info' | 'warn' | 'error';
	cooldown_seconds: number;
	enabled: boolean;
	created_at: string;
	updated_at: string;
}
export interface MetricAlertRuleInput {
	workload_id?: string;
	name: string;
	metric: 'cpu_percent' | 'memory_percent' | 'memory_bytes';
	comparator: 'gt' | 'lt';
	threshold: number;
	severity?: 'info' | 'warn' | 'error';
	cooldown_seconds?: number;
	enabled?: boolean;
}
export function listMetricAlertRules(opts?: {
	workloadID?: string;
	signal?: AbortSignal;
}): Promise<MetricAlertRule[]> {
	const params = opts?.workloadID ? `?workload_id=${encodeURIComponent(opts.workloadID)}` : '';
	return get<MetricAlertRule[]>(`/api/metric-alert-rules${params}`, opts?.signal);
}
export function getMetricAlertRule(id: number, signal?: AbortSignal): Promise<MetricAlertRule> {
	return get<MetricAlertRule>(`/api/metric-alert-rules/${id}`, signal);
}
export function createMetricAlertRule(data: MetricAlertRuleInput): Promise<MetricAlertRule> {
	return post<MetricAlertRule>('/api/metric-alert-rules', data);
}
export function updateMetricAlertRule(
	id: number,
	data: MetricAlertRuleInput
): Promise<MetricAlertRule> {
	return patch<MetricAlertRule>(`/api/metric-alert-rules/${id}`, data);
}
export function deleteMetricAlertRule(id: number): Promise<void> {
	return del<void>(`/api/metric-alert-rules/${id}`);
}

// ── Shared secrets ──────────────────────────────────────────────────
// A shared secret is a named env KEY whose value the reconciler injects
// into matching workloads. Scope model: scope="global" applies to every
// app; scope="app" + app_id narrows it to one app grouping.
//
// The value is WRITE-ONLY: the API never returns it, only a `has_value`
// presence flag. On update, omit `value` to keep the stored secret;
// provide it to set/rotate. Flipping `encrypted` requires resubmitting
// `value` (the server 400s otherwise). IDs are uuid strings.

export interface SharedSecret {
	id: string;
	name: string; // the env KEY
	has_value: boolean; // true if a value is stored (value itself is write-only, never returned)
	encrypted: boolean;
	scope: 'global' | 'app';
	app_id: string; // set when scope === 'app'
	description: string;
	enabled: boolean;
	created_at: string;
	updated_at: string;
}
export interface SharedSecretInput {
	name?: string;
	value?: string; // omit to keep the existing value on update; provide to set/rotate
	encrypted?: boolean;
	scope?: 'global' | 'app';
	app_id?: string;
	description?: string;
	enabled?: boolean;
}
export function listSharedSecrets(opts?: {
	appID?: string;
	signal?: AbortSignal;
}): Promise<SharedSecret[]> {
	const params = opts?.appID ? `?app_id=${encodeURIComponent(opts.appID)}` : '';
	return get<SharedSecret[]>(`/api/shared-secrets${params}`, opts?.signal);
}
export function getSharedSecret(id: string, signal?: AbortSignal): Promise<SharedSecret> {
	return get<SharedSecret>(`/api/shared-secrets/${id}`, signal);
}
export function createSharedSecret(data: SharedSecretInput): Promise<SharedSecret> {
	return post<SharedSecret>('/api/shared-secrets', data);
}
export function updateSharedSecret(id: string, data: SharedSecretInput): Promise<SharedSecret> {
	return patch<SharedSecret>(`/api/shared-secrets/${id}`, data);
}
export function deleteSharedSecret(id: string): Promise<void> {
	return del<void>(`/api/shared-secrets/${id}`);
}

