// Hand-transcribed from src/web/openapi.yaml. Field names stay snake_case to
// match the wire format exactly; nullability mirrors the contract.

export type Period = "1h" | "24h" | "7d" | "30d";

/**
 * Every non-2xx JSON response: `http_util.respondError` writes this one field
 * and nothing else. `ApiError.message` in api.ts reads `error` out of it.
 */
export interface ErrorEnvelope {
	error: string;
}

export interface Health {
	status: "ok" | "degraded";
	disk: {
		state: "ok" | "warn" | "critical";
		free_bytes: number;
		db_bytes: number;
		log_bytes: number;
		sample_failures: number;
	};
	upstreams: {
		available: number;
		total: number;
	};
	queries_dropped: number;
	writer_failed: boolean;
	refreshes_gated: number;
	snapshot_generation: number | null;
	/**
	 * The diagnostics store's own state, not a summary of what it holds:
	 * `unavailable` means the store is missing or its last write failed, so the
	 * counts below are the last ones it managed to observe.
	 */
	diagnostics: {
		state: "recording" | "unavailable";
		active_warnings: number;
		active_errors: number;
	};
}

export interface Version {
	version: string;
	git_commit: string;
	zig_version: string;
	uptime_seconds: number;
}

export interface LoginRequest {
	password: string;
}

export interface LoginResponse {
	authenticated: true;
	auth_required: boolean;
}

export interface LogoutResponse {
	authenticated: false;
}

export interface QueryRow {
	id: number;
	ts: number;
	domain: string;
	client_ip: string;
	qtype: number | null;
	blocked: boolean;
	block_reason: string;
	response_time_us: number | null;
	cache_hit: boolean | null;
	upstream: string;
}

/** SSE `event: query` payload: a QueryRow minus `id` (precedes persistence). */
export type LiveQueryEvent = Omit<QueryRow, "id">;

export interface QueriesPage {
	queries: QueryRow[];
	next_before: number | null;
}

export interface QueriesFilter {
	limit?: number;
	before?: number;
	domain?: string;
	client?: string;
	blocked?: boolean;
	since?: number;
	until?: number;
}

/**
 * The fifteen operational event codes, in the order `src/storage/events.zig`
 * declares them. A value, not only a type, because the copy map has to be
 * proven exhaustive at runtime as well as by `tsc`.
 */
export const DIAGNOSTIC_CODES = [
	"disk.space",
	"disk.probe",
	"blocklist.refresh",
	"blocklist.snapshot",
	"blocklist.storage",
	"certificate.reload",
	"query_log.write",
	"query_log.maintenance",
	"query_log.recreated",
	"upstream_history.write",
	"upstream.exchange",
	"client_names.storage",
	"clients.storage",
	"listener.start",
	"configuration.load",
] as const;

export type DiagnosticCode = (typeof DIAGNOSTIC_CODES)[number];

export type DiagnosticSeverity = "warning" | "error";

/** Which episodes a query selects. `all` is the server's default. */
export type DiagnosticState = "active" | "resolved" | "all";

export interface DiagnosticEvent {
	id: number;
	code: DiagnosticCode;
	/** The part of `code` before the dot, repeated by the server for filtering. */
	component: string;
	/** The display identity of what failed; redacted where it derives from a url. */
	subject: string;
	severity: DiagnosticSeverity;
	first_seen: number;
	last_seen: number;
	occurrences: number;
	/** Null while the episode is still open. */
	resolved_at: number | null;
	detail: string;
}

export interface DiagnosticsPage {
	events: DiagnosticEvent[];
	next_before: number | null;
	/** Episodes open right now, whatever this page filtered to. */
	active: {
		warnings: number;
		errors: number;
	};
}

/** `DELETE /api/diagnostics` — how many resolved events the purge removed. */
export interface DiagnosticsPurge {
	purged: number;
}

export interface DiagnosticsFilter {
	state?: DiagnosticState;
	severity?: DiagnosticSeverity;
	component?: string;
	since?: number;
	until?: number;
	limit?: number;
	before?: number;
}

export interface StatsTotals {
	period: Period;
	since: number;
	until: number;
	queries: number;
	blocked: number;
	cached: number;
	clients: number;
	avg_response_time_us: number | null;
}

export interface Bucket {
	ts: number;
	queries: number;
	blocked: number;
	cached: number;
}

export interface StatsTimeseries {
	period: Period;
	since: number;
	until: number;
	bucket_seconds: number;
	buckets: Bucket[];
}

export interface LookupResult {
	domain: string;
	group_id: number;
	local_records: boolean;
	forward_zone: string | null;
	blocked: boolean;
	reason: string;
	matched: string;
	source_url: string | null;
	safe_search_rewrite: string | null;
}

export interface UpstreamPeriodStats {
	attempts: number;
	successes: number;
	failures: number;
	/** successes/attempts, 0 to 1; null when attempts is 0 — no observations is not perfect reliability. */
	success_rate: number | null;
	/** The newest failure inside the window, unix seconds; null when the window holds none. */
	last_failure_at: number | null;
	/** The error name belonging to last_failure_at; null exactly when it is. */
	last_failure_error: string | null;
}

export interface UpstreamHealthEntry {
	url: string;
	/** Live configuration, not history. */
	enabled: boolean;
	/** Live state; false while the upstream is backing off. */
	available: boolean;
	period: UpstreamPeriodStats;
}

export interface UpstreamHealth {
	period: Period;
	since: number;
	until: number;
	available: number;
	total: number;
	/**
	 * No capacity drops known in this process within the selected window; up to about a minute of
	 * the newest outcomes may not have flushed yet, and outcomes lost in an unclean shutdown are
	 * not detectable.
	 */
	complete: boolean;
	upstreams: UpstreamHealthEntry[];
}

export interface Group {
	id: number;
	name: string;
	safe_search: boolean;
}

export interface GroupInput {
	name: string;
	safe_search?: boolean;
}

export interface GroupSources {
	source_ids: number[];
}

export interface Blocklist {
	id: number;
	url: string;
	name: string;
	enabled: boolean;
	is_suggested: boolean;
	last_updated: number | null;
	domain_count: number;
	wildcard_count: number;
	exception_count: number;
	skipped_regex_count: number;
	skipped_unsupported_count: number;
	checksum: string | null;
}

export interface BlocklistInput {
	url: string;
	name: string;
	enabled?: boolean;
	is_suggested?: boolean;
}

export interface BlocklistEcho {
	id: number;
	url: string;
	name: string;
	enabled: boolean;
	is_suggested: boolean;
}

export interface SourceStatus {
	id: number;
	state: string;
	loaded: boolean;
	last_attempt: number;
	last_success: number;
	url: string;
	last_error: string;
	domains: number;
	wildcards: number;
	exceptions: number;
	skipped_regex: number;
	skipped_unsupported: number;
}

export type RuleKind = "exact" | "wildcard" | "regex";
export type RuleAction = "allow" | "block";

export interface Rule {
	id: number;
	group_id: number;
	group: string;
	pattern: string;
	kind: RuleKind;
	action: RuleAction;
	created_at: number;
}

export interface RuleInput {
	group_id: number;
	pattern: string;
	kind: RuleKind;
	action: RuleAction;
}

export interface RuleEcho {
	id: number;
	group_id: number;
	pattern: string;
	kind: RuleKind;
	action: RuleAction;
}

export type LocalRecordType = "A" | "AAAA" | "CNAME";

export interface LocalRecord {
	id: number;
	name: string;
	rtype: LocalRecordType;
	value: string;
	ttl: number;
}

export interface LocalRecordInput {
	name: string;
	rtype: LocalRecordType;
	value: string;
	ttl?: number;
}

export interface ForwardZone {
	id: number;
	zone: string;
	resolver: string;
}

export interface ForwardZoneInput {
	zone: string;
	resolver: string;
}

export interface Client {
	id: number;
	ip: string;
	name: string;
	/** Learned over reverse DNS. `name` wins whenever it is non-empty. */
	learned_name: string;
	group_id: number;
	group: string;
	hand_edited: boolean;
	first_seen: number;
	last_seen: number;
}

export interface ClientEdit {
	name?: string;
	group_id: number;
}

export interface ClientPrefix {
	id: number;
	prefix: string;
	group_id: number;
	group: string;
	priority: number;
}

export interface ClientPrefixInput {
	prefix: string;
	group_id: number;
	priority?: number;
}

export interface Upstream {
	id: number;
	url: string;
	priority: number;
	enabled: boolean;
	tls_name: string;
}

export interface UpstreamInput {
	url: string;
	priority?: number;
	enabled?: boolean;
	tls_name?: string;
}

export interface UpstreamEcho {
	id: number;
	url: string;
	priority: number;
	enabled: boolean;
	tls_name: string;
	restart_required: true;
}

export interface PauseState {
	paused: boolean;
	until: number | null;
}

export interface PausePost {
	paused: boolean;
	duration_seconds?: number | null;
}

export interface TlsListenerSettings {
	enabled: boolean;
	bind: string;
	port: number;
	cert_path: string;
	key_path: string;
}

export interface Settings {
	upstream: {
		attempt_timeout_ms: number;
		read_timeout_ms: number;
		total_timeout_ms: number;
	};
	dns: {
		bind_ipv4: string;
		bind_ipv6: string;
		port: number;
		rate_limit: number;
		rate_window_seconds: number;
	};
	blocking: {
		response: "zero" | "nxdomain";
		ttl: number;
	};
	cache: {
		size: number;
		negative_ttl_max: number;
	};
	web: {
		enabled: boolean;
		bind: string;
		port: number;
		session_ttl_hours: number;
		api_rate_limit_per_min: number;
		api_localhost_exempt: boolean;
		sse_max_connections_per_ip: number;
		/** Comma-separated IP literals; empty trusts no proxy's X-Forwarded-For. */
		trusted_proxies: string;
		/** Derived, read-only; true iff a password hash is stored. Never sent back. */
		auth_enabled: boolean;
	};
	doh_server: TlsListenerSettings;
	dot_server: TlsListenerSettings;
	edns: {
		ecs_mode: "strip" | "forward";
	};
	logging: {
		level: "error" | "warn" | "info" | "debug";
		retention_days: number;
		query_log_buffer_max: number;
		query_log_flush_interval_s: number;
		hide_domains: boolean;
		hide_client_ips: boolean;
		output: "stderr" | "syslog" | "file";
		file_path: string;
		max_size_mb: number;
		max_files: number;
	};
	disk: {
		min_free_mb: number;
		warn_free_mb: number;
	};
	blocklist_update: {
		enabled: boolean;
		interval_hours: number;
	};
}

/**
 * Which configuration source the running process obeys. `path` and
 * `reconciled_at` are non-null only under `managed_file`: the file the process
 * loaded, and the epoch second at which it loaded it. Authority lives in the
 * invocation, never in the database, so this is the only place the UI can read
 * it — and it rides an authenticated route, never the open ones.
 */
export interface Authority {
	mode: "database" | "managed_file";
	path: string | null;
	reconciled_at: number | null;
}

export interface SettingsEnvelope {
	settings: Settings;
	restart_required: string[];
	authority: Authority;
}

export interface TlsListenerPatch {
	enabled?: boolean;
	bind?: string;
	port?: number;
	cert_path?: string;
	key_path?: string;
}

/** Partial update; `web.password` is write-only, `web.auth_enabled` is never sent. */
export interface SettingsPatch {
	upstream?: Partial<Settings["upstream"]>;
	dns?: Partial<Settings["dns"]>;
	blocking?: Partial<Settings["blocking"]>;
	cache?: Partial<Settings["cache"]>;
	web?: Partial<Omit<Settings["web"], "auth_enabled">> & { password?: string };
	doh_server?: TlsListenerPatch;
	dot_server?: TlsListenerPatch;
	edns?: Partial<Settings["edns"]>;
	logging?: Partial<Settings["logging"]>;
	disk?: Partial<Settings["disk"]>;
	blocklist_update?: Partial<Settings["blocklist_update"]>;
}
