/**
 * One query, explained in the order it met the pipeline.
 *
 * This is the body of the detail surface, with no route in it, because two
 * surfaces show it: the persisted detail page, which fetched the row by id, and
 * a live row, whose provenance arrived in the stream frame and which SQLite may
 * not have written yet. The related actions are links into routes, so the
 * caller passes them in already built.
 */

import type { ReactNode } from "react";
import * as stylex from "@stylexjs/stylex";
import { formatMicros, formatTime } from "@/lib/format";
import type { PolicyReason, Provenance } from "@/lib/types";
import { clientLabel, useClientNames } from "@/features/clients/clientNames";
import {
	policyActionLabel,
	policyReasonLabel,
	qclassName,
	rcodeName,
	routeKindLabel,
} from "@/features/provenance/provenanceCopy";
import { qtypeName } from "@/features/provenance/qtype";
import { styles as shared } from "@/ui/styles";
import { colors } from "@/ui/tokens.stylex";

const styles = stylex.create({
	heading: {
		marginTop: "0.5rem",
		fontSize: "1.5rem",
		lineHeight: "2rem",
		fontWeight: 600,
		wordBreak: "break-all",
	},
	subtitle: {
		marginTop: "0.25rem",
		color: colors.textSecondary,
	},
	/** The recorded facts, fenced off from the live links below them. */
	record: {
		marginTop: "1rem",
		maxWidth: "48rem",
		borderRadius: "0.25rem",
		borderWidth: 1,
		borderStyle: "solid",
		borderColor: colors.border,
		backgroundColor: colors.surfaceRaised,
		padding: "1rem",
	},
	recordNote: {
		fontSize: "0.8125rem",
		lineHeight: "1.25rem",
		color: colors.textMuted,
	},
	section: {
		marginTop: "1rem",
		borderTopWidth: { default: 1, ":first-of-type": 0 },
		borderTopStyle: "solid",
		borderTopColor: colors.border,
		paddingTop: { default: "1rem", ":first-of-type": 0 },
	},
	sectionHeading: {
		fontSize: "0.75rem",
		lineHeight: "1rem",
		fontWeight: 600,
		letterSpacing: "0.05em",
		textTransform: "uppercase",
		color: colors.textMuted,
	},
	facts: {
		marginTop: "0.5rem",
		marginBottom: 0,
		display: "grid",
		gap: "0.375rem 1rem",
		gridTemplateColumns: {
			default: "auto",
			"@media (min-width: 640px)": "max-content 1fr",
		},
		fontSize: "0.875rem",
		lineHeight: "1.25rem",
	},
	term: {
		color: colors.textMuted,
	},
	value: {
		margin: 0,
		wordBreak: "break-all",
	},
	muted: {
		color: colors.textMuted,
	},
	related: {
		marginTop: "1.5rem",
		maxWidth: "48rem",
	},
	relatedHeading: {
		fontSize: "1.125rem",
		lineHeight: "1.75rem",
		fontWeight: 600,
	},
	relatedNote: {
		marginTop: "0.25rem",
		fontSize: "0.875rem",
		lineHeight: "1.25rem",
		color: colors.textMuted,
	},
	relatedList: {
		marginTop: "0.5rem",
		display: "flex",
		flexWrap: "wrap",
		gap: "0.75rem",
		fontSize: "0.875rem",
		lineHeight: "1.25rem",
	},
	link: {
		color: colors.primaryOnSurface,
	},
});

/** The look of one related action, so every caller's links match. */
export const provenanceRelatedLink = styles.link;

function Section({ title, children }: { title: string; children: ReactNode }) {
	return (
		<div {...stylex.props(styles.section)}>
			<h2 {...stylex.props(styles.sectionHeading)}>{title}</h2>
			<dl {...stylex.props(styles.facts)}>{children}</dl>
		</div>
	);
}

function Fact({ label, children }: { label: string; children: ReactNode }) {
	return (
		<>
			<dt {...stylex.props(styles.term)}>{label}</dt>
			<dd {...stylex.props(styles.value)}>{children}</dd>
		</>
	);
}

/**
 * A recorded name, in the face the rest of the interface gives to names. It
 * wraps the value rather than the row, so the prose that stands in for a
 * missing one is not set in the same typewriter face.
 */
function Mono({ children }: { children: string }) {
	return <span {...stylex.props(shared.mono)}>{children}</span>;
}

/** An empty text field means the server recorded nothing there, never an empty value. */
function Absent({ children }: { children: string }) {
	return <span {...stylex.props(styles.muted)}>{children}</span>;
}

/**
 * What an empty `matched` means. `no_match` is the only reason the matcher
 * itself records with nothing to show; every other empty one names a pipeline
 * step that answered before filtering — a local record, a forward zone, a pause
 * (see `PolicyReason` in src/storage/provenance.zig) — where "nothing matched"
 * would claim an evaluation that never happened.
 */
function unmatchedLabel(reason: PolicyReason): string {
	return reason === "no_match" ? "Nothing matched" : "The matcher never ran";
}

interface Props {
	provenance: Provenance;
	/**
	 * The log row this explains, or null for a frame read straight off the
	 * stream. Null is the same fact `QuerySummary.id` carries: the event
	 * precedes its own insert, so there is no row to name and none is invented.
	 */
	persistedId: number | null;
	/** The related-action links, built by whichever surface owns the routes. */
	relatedActions: ReactNode;
}

export default function ProvenanceDetail({ provenance, persistedId, relatedActions }: Props) {
	const { request, group, policy, rewrites, route, response } = provenance;
	const clientNames = useClientNames();
	const currentClient = clientLabel(request.client, clientNames);

	return (
		<>
			<h1 {...stylex.props(styles.heading, shared.mono)}>{request.domain}</h1>
			<p {...stylex.props(styles.subtitle)}>
				{formatTime(request.time)} — {policyActionLabel(policy.action)}
				{/* The verdict alone reads as a success; a non-NOERROR answer says otherwise. */}
				{response.rcode !== 0 && ` — ${rcodeName(response.rcode)}`}
			</p>

			<div {...stylex.props(styles.record)}>
				<p {...stylex.props(styles.recordNote)}>
					What was recorded when this query was answered. Group and blocklist names are the ones in force at
					that moment; they may have been renamed or deleted since.
					{persistedId === null &&
						" This is the event as it was streamed; the query log may not have written it yet."}
				</p>

				<Section title="Request">
					<Fact label="Time">{formatTime(request.time)}</Fact>
					<Fact label="Domain">
						<Mono>{request.domain}</Mono>
					</Fact>
					<Fact label="Client">
						<Mono>{request.client}</Mono>
					</Fact>
					<Fact label="Type">{qtypeName(request.qtype)}</Fact>
					<Fact label="Class">{qclassName(request.qclass)}</Fact>
				</Section>

				<Section title="Group">
					<Fact label="Name">{group.name === "" ? <Absent>No group recorded</Absent> : group.name}</Fact>
					<Fact label="Id">{group.id === null ? <Absent>—</Absent> : group.id}</Fact>
				</Section>

				<Section title="Policy">
					<Fact label="Decision">{policyActionLabel(policy.action)}</Fact>
					<Fact label="Reason">{policyReasonLabel(policy.reason)}</Fact>
					<Fact label="Matched">
						{policy.matched === "" ? (
							<Absent>{unmatchedLabel(policy.reason)}</Absent>
						) : (
							<Mono>{policy.matched}</Mono>
						)}
					</Fact>
					<Fact label="Blocklist">
						{policy.source_name === "" ? (
							<Absent>Not a blocklist decision</Absent>
						) : policy.source_id === null ? (
							policy.source_name
						) : (
							`${policy.source_name} (#${policy.source_id})`
						)}
					</Fact>
				</Section>

				<Section title="Rewrites">
					<Fact label="CNAME target">
						{rewrites.cname_target === "" ? (
							<Absent>The queried name was decided directly</Absent>
						) : (
							<Mono>{rewrites.cname_target}</Mono>
						)}
					</Fact>
					<Fact label="Safe search">
						{rewrites.safe_search_target === "" ? (
							<Absent>No rewrite</Absent>
						) : (
							<Mono>{rewrites.safe_search_target}</Mono>
						)}
					</Fact>
				</Section>

				<Section title="Route">
					<Fact label="Answered by">{routeKindLabel(route.kind)}</Fact>
					<Fact label="Forward zone">
						{route.forward_zone === "" ? <Absent>—</Absent> : <Mono>{route.forward_zone}</Mono>}
					</Fact>
					<Fact label="Upstream">
						{route.upstream === "" ? <Absent>No upstream exchange</Absent> : <Mono>{route.upstream}</Mono>}
					</Fact>
				</Section>

				<Section title="Response">
					<Fact label="Result">{rcodeName(response.rcode)}</Fact>
					<Fact label="Took">
						{response.duration_us === null ? (
							<Absent>Not measured</Absent>
						) : (
							formatMicros(response.duration_us)
						)}
					</Fact>
				</Section>
			</div>

			{/* A named region, because the Pause it may offer is not the only Pause
			    on screen: the sidebar carries one too, and the two answer different
			    questions. */}
			<section aria-labelledby="related-actions" {...stylex.props(styles.related)}>
				<h2 id="related-actions" {...stylex.props(styles.relatedHeading)}>
					Related
				</h2>
				<p {...stylex.props(styles.relatedNote)}>
					These read the current configuration, which may no longer be the one that decided this query.
				</p>
				{currentClient !== null && (
					<p {...stylex.props(styles.relatedNote)}>
						{currentClient.learned ? (
							<>
								Reverse DNS currently resolves <Mono>{request.client}</Mono> to{" "}
								<Mono>{currentClient.text}</Mono>.
							</>
						) : (
							<>
								The client list currently names <Mono>{request.client}</Mono> “{currentClient.text}”.
							</>
						)}
					</p>
				)}
				<div {...stylex.props(styles.relatedList)}>{relatedActions}</div>
			</section>
		</>
	);
}
