// 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;
}

/**
 * The five conditions `GET /api/health` reports, and a `status` computed from
 * exactly their states. Nothing degrades the rollup without appearing here, so
 * a reader of this object can always name what degraded the box.
 */
export interface Health {
	status: "ok" | "degraded";
	/**
	 * Is filtering in force. `unavailable` is not an operator's doing: it is the
	 * state in which the query path has no filter snapshot to evaluate against.
	 * It outranks a pause, which is why `until` is null under it.
	 */
	protection: {
		state: "active" | "paused" | "unavailable";
		/** The second filtering resumes at; null for an indefinite pause and for every other state. */
		until: number | null;
	};
	upstreams: {
		state: "ok" | "unavailable";
		available: number;
		/** Enabled upstreams: the pool is built from those alone. */
		total: number;
	};
	/**
	 * Whether Activity can be trusted. `dropped_total` is cumulative and does not
	 * decide the state — a drop an hour ago is not a fault now.
	 */
	query_history: {
		state: "recording" | "losing" | "failed";
		dropped_total: number;
		/** The newest drop; stamped by a separate atomic, so it can lag a non-zero count. */
		last_drop_s: 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;
	};
	/** `low` is the monitor's `warn` renamed at the wire: warn reads as a log level. */
	disk: {
		state: "ok" | "low" | "critical";
		free_bytes: 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;
}

/**
 * The three closed enums `src/storage/provenance.zig` stores, as values rather
 * than bare types: the copy maps in `features/provenance/provenanceCopy.ts` have to
 * be proven exhaustive at runtime as well as by `tsc`, exactly as
 * `DIAGNOSTIC_CODES` below.
 */
export const POLICY_ACTIONS = ["not_evaluated", "allow", "block"] as const;
export type PolicyAction = (typeof POLICY_ACTIONS)[number];

export const POLICY_REASONS = [
	"rule_allow_exact",
	"rule_block_exact",
	"rule_allow_wildcard",
	"rule_block_wildcard",
	"rule_allow_regex",
	"rule_block_regex",
	"blocklist_exception",
	"blocklist_domain",
	"blocklist_wildcard",
	"local_record",
	"forward_zone",
	"non_in_class",
	"paused",
	"snapshot_unavailable",
	"no_match",
	"protocol_error",
] as const;
export type PolicyReason = (typeof POLICY_REASONS)[number];

export const ROUTE_KINDS = ["blocked", "local", "forward_zone", "upstream", "cache", "rejected"] as const;
export type RouteKind = (typeof ROUTE_KINDS)[number];

/** The summary projection the query-log table scans; full provenance is at `/api/queries/{id}`. */
export interface QueryRow {
	id: number;
	ts: number;
	domain: string;
	client_ip: string;
	qtype: number | null;
	qclass: number;
	rcode: number;
	blocked: boolean;
	response_time_us: number | null;
	cache_hit: boolean | null;
	upstream: string;
	policy_action: PolicyAction;
	policy_reason: PolicyReason;
	route_kind: RouteKind;
}

export interface ProvenanceRequest {
	time: number;
	domain: string;
	client: string;
	qtype: number | null;
	qclass: number;
}

/** The group as a historical fact: the id may name a group since renamed or deleted. */
export interface ProvenanceGroup {
	id: number | null;
	name: string;
}

export interface ProvenancePolicy {
	action: PolicyAction;
	reason: PolicyReason;
	matched: string;
	source_id: number | null;
	source_name: string;
}

export interface ProvenanceRewrites {
	cname_target: string;
	safe_search_target: string;
}

export interface ProvenanceRoute {
	kind: RouteKind;
	forward_zone: string;
	/** Non-empty only for an attempted upstream or forward-zone exchange; already redacted. */
	upstream: string;
}

export interface ProvenanceResponse {
	/** The twelve-bit EDNS extended code, not the four header bits alone. */
	rcode: number;
	duration_us: number | null;
}

/**
 * One query, fully explained, in the order a query meets the pipeline
 * (`src/web/provenance_view.zig`). Every text field follows the repository
 * convention: `""` means absent.
 */
export interface Provenance {
	request: ProvenanceRequest;
	group: ProvenanceGroup;
	policy: ProvenancePolicy;
	rewrites: ProvenanceRewrites;
	route: ProvenanceRoute;
	response: ProvenanceResponse;
}

/** `GET /api/queries/{id}`: the same six groups plus the row id. */
export interface QueryDetail extends Provenance {
	id: number;
}

/** SSE `event: query` payload: the full provenance, minus an id it cannot have yet. */
export type LiveQueryEvent = Provenance;

/**
 * How much of the requested window the query log can answer for. Retention
 * deletes rows and advances the watermark in one transaction, so an empty
 * window is distinguishable from a pruned one.
 */
export interface Coverage {
	complete: boolean;
	/** The oldest instant the log is complete for, unix seconds. */
	available_since: number;
}

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

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;
}

/** The window's four headline numbers. */
export interface OverviewTotals {
	queries: number;
	blocked: number;
	/** Distinct clients seen in the window, not a sum of per-bucket counts. */
	clients: number;
	avg_response_time_us: number | null;
}

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

/**
 * One DNS type's share of the window. `qtype` is the numeric code as logged:
 * naming it is the admin's job (`features/provenance/qtype.ts`), and a row whose
 * type was never recorded keeps its own `null` group rather than disappearing.
 */
export interface OverviewTypeRow {
	qtype: number | null;
	count: number;
}

/**
 * How the window's queries were answered. `source` names the answering upstream
 * on `upstream` rows and the zone on `forward_zone` rows; every other route kind
 * carries null, as does a row whose identity was not recorded.
 */
export interface OverviewRouteRow {
	route: RouteKind;
	source: string | null;
	count: number;
}

/** One client's per-bucket counts, aligned to `Overview.buckets`. */
export interface OverviewClientSeries {
	client: string;
	buckets: number[];
}

/**
 * Everything the Overview page draws, for one window, from one request. The
 * server answers all six panels out of a single read transaction, so the
 * headline totals, the two timelines and the two breakdowns are guaranteed to
 * describe the same span *and* the same database state — a coherence the five
 * endpoints this replaces could not offer.
 */
export interface Overview {
	period: Period;
	since: number;
	until: number;
	bucket_seconds: number;
	totals: OverviewTotals;
	buckets: Bucket[];
	/** The eight busiest clients in the window, ranked by total count. */
	clients: OverviewClientSeries[];
	/** Everything outside the top eight. Always present and always bucket-count-sized. */
	other: number[];
	types: OverviewTypeRow[];
	routes: OverviewRouteRow[];
	coverage: Coverage;
}

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 Group {
	id: number;
	name: string;
	safe_search: boolean;
}

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

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: false;
}

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;
	};
}

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

/**
 * Which configuration source the running process obeys, and whether it owes a
 * restart. `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.
 * Both facts live in the invocation and the process, never in the database, so
 * `/api/config/status` is the only place the UI can read them — and it is an
 * authenticated route, never one of the open ones.
 *
 * `restart_pending` is true once the server has committed a change only a
 * restart applies. Every settings key applies live except the listener binds
 * and `web.enabled`, so those are the only writes that raise it. Nothing but
 * process exit clears it, so a browser reload cannot dismiss it.
 */
export interface ConfigStatus {
	authority: "database" | "managed_file";
	path: string | null;
	reconciled_at: number | null;
	restart_pending: boolean;
}

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"]>;
}

/** One endpoint's outcome from `POST /api/certs/reload`. */
export interface CertReloadOutcome {
	enabled: boolean;
	reloaded: boolean;
	/** Why the reload failed; null on success and while the endpoint is disabled. */
	error: string | null;
}

/**
 * The reload runs per endpoint and always answers 200: a failed reload is an
 * outcome, not an error, and the previous certificate keeps serving.
 */
export interface CertsReload {
	doh: CertReloadOutcome;
	dot: CertReloadOutcome;
}
