// Responsibility: Typed YAML profile parsing for repo-owned Cloud Console browser closeout.

export interface HwlabRuntimeWebProbeConsoleViewportSpec {
  readonly id: string;
  readonly width: number;
  readonly height: number;
}

export interface HwlabRuntimeWebProbeConsoleRouteSpec {
  readonly id: string;
  readonly path: string;
  readonly readySelector: string;
  readonly screenshot: boolean;
}

export interface HwlabRuntimeWebProbeConsoleProjectsWorkflowSpec {
  readonly routeId: string;
  readonly listButtonName: string;
  readonly viewQueryKey: string;
}

export interface HwlabRuntimeWebProbeConsoleHwpodWorkflowSpec {
  readonly devicesRouteId: string;
  readonly nodesRouteId: string;
  readonly onboardingRouteId: string;
  readonly listButtonName: string;
  readonly cardButtonName: string;
  readonly devicesListSelector: string;
  readonly devicesCardSelector: string;
  readonly nodesListSelector: string;
  readonly pcbTraceSelector: string;
  readonly detailButtonName: string;
  readonly detailPathPrefix: string;
  readonly onboardingCommandSelector: string;
  readonly requiredCommandPrefixes: readonly string[];
  readonly downloadSelector: string;
  readonly forbiddenDownloadSubstrings: readonly string[];
  readonly forbiddenInternalSelector: string;
}

export interface HwlabRuntimeWebProbeConsoleMdtodoWorkflowSpec {
  readonly routeId: string;
  readonly taskSelector: string;
  readonly taskDeepLinkSegment: string;
  readonly taskDetailSelector: string;
  readonly reportLinkSelector: string;
  readonly reportPreviewSelector: string;
  readonly fullscreenButtonSelector: string;
  readonly fullscreenDialogSelector: string;
}

export interface HwlabRuntimeWebProbeConsoleTasktreeWorkflowSpec {
  readonly routeId: string;
  readonly taskSelector: string;
  readonly barSelector: string;
  readonly milestoneSelector: string;
  readonly ganttSelector: string;
  readonly scrollSelector: string;
  readonly labelPaneSelector: string;
  readonly scaleSelector: string;
  readonly tickSelector: string;
  readonly zoomInButtonName: string;
  readonly secondsScaleLabel: string;
  readonly zoomScreenshotName: string;
  readonly detailSelector: string;
  readonly dialogSelector: string;
  readonly dockedSelector: string;
  readonly reportSelector: string;
  readonly detailScreenshotName: string;
}

export interface HwlabRuntimeWebProbeConsoleAgentViewSpec {
  readonly buttonName: string;
  readonly queryValue: string;
  readonly screenshotName: string;
}

export interface HwlabRuntimeWebProbeConsoleAgentSelectionSpec {
  readonly selector: string;
  readonly inspectorSelector: string;
  readonly screenshotName: string;
}

export interface HwlabRuntimeWebProbeConsoleAgentObserverWorkflowSpec {
  readonly routeId: string;
  readonly eventSourcePath: string;
  readonly phaseSelector: string;
  readonly blockedPhases: readonly string[];
  readonly streamSettleMs: number;
  readonly views: readonly HwlabRuntimeWebProbeConsoleAgentViewSpec[];
  readonly selection?: HwlabRuntimeWebProbeConsoleAgentSelectionSpec;
}

export interface HwlabRuntimeWebProbeConsoleVerificationProfileSpec {
  readonly navigationTimeoutMs: number;
  readonly settleMs: number;
  readonly commandTimeoutSeconds: number;
  readonly minTextLength: number;
  readonly maxHorizontalOverflowPx: number;
  readonly outputLimits: {
    readonly failures: number;
    readonly pageErrors: number;
    readonly consoleErrors: number;
    readonly requestFailures: number;
    readonly responseErrors: number;
    readonly screenshots: number;
    readonly textPreviewChars: number;
  };
  readonly viewports: readonly HwlabRuntimeWebProbeConsoleViewportSpec[];
  readonly routes: readonly HwlabRuntimeWebProbeConsoleRouteSpec[];
  readonly workflows: {
    readonly projects?: HwlabRuntimeWebProbeConsoleProjectsWorkflowSpec;
    readonly hwpod?: HwlabRuntimeWebProbeConsoleHwpodWorkflowSpec;
    readonly mdtodo?: HwlabRuntimeWebProbeConsoleMdtodoWorkflowSpec;
    readonly tasktree?: HwlabRuntimeWebProbeConsoleTasktreeWorkflowSpec;
    readonly agentObserver?: HwlabRuntimeWebProbeConsoleAgentObserverWorkflowSpec;
  };
}

export function parseWebProbeConsoleVerificationProfiles(
  value: unknown,
  path: string,
): Readonly<Record<string, HwlabRuntimeWebProbeConsoleVerificationProfileSpec>> {
  const raw = record(value, path);
  const entries = Object.entries(raw);
  if (entries.length < 1 || entries.length > 20) throw new Error(`${path} must contain 1-20 profiles`);
  return Object.fromEntries(entries.map(([name, profile]) => {
    simpleId(name, `${path}.${name}`);
    return [name, consoleProfile(profile, `${path}.${name}`)];
  }));
}

function consoleProfile(value: unknown, path: string): HwlabRuntimeWebProbeConsoleVerificationProfileSpec {
  const raw = record(value, path);
  onlyKeys(raw, [
    "navigationTimeoutMs",
    "settleMs",
    "commandTimeoutSeconds",
    "minTextLength",
    "maxHorizontalOverflowPx",
    "outputLimits",
    "viewports",
    "routes",
    "workflows",
  ], path);
  const viewports = array(raw.viewports, `${path}.viewports`, 1, 8).map((item, index) => viewport(item, `${path}.viewports[${index}]`));
  const routes = array(raw.routes, `${path}.routes`, 1, 100).map((item, index) => route(item, `${path}.routes[${index}]`));
  unique(viewports.map((item) => item.id), `${path}.viewports[].id`);
  unique(routes.map((item) => item.id), `${path}.routes[].id`);
  unique(routes.map((item) => item.path), `${path}.routes[].path`);
  const routeIds = new Set(routes.map((item) => item.id));
  const workflows = workflowConfig(raw.workflows, `${path}.workflows`, routeIds);
  const output = record(raw.outputLimits, `${path}.outputLimits`);
  onlyKeys(output, [
    "failures", "pageErrors", "consoleErrors", "requestFailures", "responseErrors", "screenshots", "textPreviewChars",
  ], `${path}.outputLimits`);
  const screenshotBudget = integer(output.screenshots, `${path}.outputLimits.screenshots`, 1, 1000);
  const matrixScreenshots = routes.filter((item) => item.screenshot).length * viewports.length;
  const workflowScreenshots = (workflows.agentObserver?.views.length ?? 0)
    + (workflows.agentObserver?.selection === undefined ? 0 : 1)
    + (workflows.tasktree === undefined ? 0 : 1);
  if (matrixScreenshots + workflowScreenshots > screenshotBudget) {
    throw new Error(`${path}.outputLimits.screenshots=${screenshotBudget} is below the declared matrix/workflow screenshot count ${matrixScreenshots + workflowScreenshots}`);
  }
  return {
    navigationTimeoutMs: integer(raw.navigationTimeoutMs, `${path}.navigationTimeoutMs`, 1000, 120_000),
    settleMs: integer(raw.settleMs, `${path}.settleMs`, 0, 30_000),
    commandTimeoutSeconds: integer(raw.commandTimeoutSeconds, `${path}.commandTimeoutSeconds`, 60, 3600),
    minTextLength: integer(raw.minTextLength, `${path}.minTextLength`, 1, 100_000),
    maxHorizontalOverflowPx: integer(raw.maxHorizontalOverflowPx, `${path}.maxHorizontalOverflowPx`, 0, 1000),
    outputLimits: {
      failures: integer(output.failures, `${path}.outputLimits.failures`, 1, 1000),
      pageErrors: integer(output.pageErrors, `${path}.outputLimits.pageErrors`, 1, 1000),
      consoleErrors: integer(output.consoleErrors, `${path}.outputLimits.consoleErrors`, 1, 1000),
      requestFailures: integer(output.requestFailures, `${path}.outputLimits.requestFailures`, 1, 1000),
      responseErrors: integer(output.responseErrors, `${path}.outputLimits.responseErrors`, 1, 1000),
      screenshots: screenshotBudget,
      textPreviewChars: integer(output.textPreviewChars, `${path}.outputLimits.textPreviewChars`, 20, 2000),
    },
    viewports,
    routes,
    workflows,
  };
}

function viewport(value: unknown, path: string): HwlabRuntimeWebProbeConsoleViewportSpec {
  const raw = record(value, path);
  onlyKeys(raw, ["id", "width", "height"], path);
  return {
    id: simpleId(text(raw.id, `${path}.id`), `${path}.id`),
    width: integer(raw.width, `${path}.width`, 240, 7680),
    height: integer(raw.height, `${path}.height`, 240, 4320),
  };
}

function route(value: unknown, path: string): HwlabRuntimeWebProbeConsoleRouteSpec {
  const raw = record(value, path);
  onlyKeys(raw, ["id", "path", "readySelector", "screenshot"], path);
  return {
    id: simpleId(text(raw.id, `${path}.id`), `${path}.id`),
    path: absolutePath(text(raw.path, `${path}.path`), `${path}.path`),
    readySelector: boundedText(raw.readySelector, `${path}.readySelector`, 500),
    screenshot: boolean(raw.screenshot, `${path}.screenshot`),
  };
}

function workflowConfig(
  value: unknown,
  path: string,
  routeIds: ReadonlySet<string>,
): HwlabRuntimeWebProbeConsoleVerificationProfileSpec["workflows"] {
  const raw = record(value, path);
  onlyKeys(raw, ["projects", "hwpod", "mdtodo", "tasktree", "agentObserver"], path);
  return {
    ...(raw.projects === undefined ? {} : { projects: projectsWorkflow(raw.projects, `${path}.projects`, routeIds) }),
    ...(raw.hwpod === undefined ? {} : { hwpod: hwpodWorkflow(raw.hwpod, `${path}.hwpod`, routeIds) }),
    ...(raw.mdtodo === undefined ? {} : { mdtodo: mdtodoWorkflow(raw.mdtodo, `${path}.mdtodo`, routeIds) }),
    ...(raw.tasktree === undefined ? {} : { tasktree: tasktreeWorkflow(raw.tasktree, `${path}.tasktree`, routeIds) }),
    ...(raw.agentObserver === undefined ? {} : { agentObserver: agentObserverWorkflow(raw.agentObserver, `${path}.agentObserver`, routeIds) }),
  };
}

function tasktreeWorkflow(value: unknown, path: string, routeIds: ReadonlySet<string>): HwlabRuntimeWebProbeConsoleTasktreeWorkflowSpec {
  const raw = record(value, path);
  onlyKeys(raw, [
    "routeId", "taskSelector", "barSelector", "milestoneSelector", "ganttSelector",
    "scrollSelector", "labelPaneSelector",
    "scaleSelector", "tickSelector", "zoomInButtonName", "secondsScaleLabel", "zoomScreenshotName",
    "detailSelector", "dialogSelector", "dockedSelector", "reportSelector", "detailScreenshotName",
  ], path);
  const detailScreenshotName = boundedText(raw.detailScreenshotName, `${path}.detailScreenshotName`, 120);
  if (!/^[A-Za-z0-9._-]+\.png$/u.test(detailScreenshotName)) throw new Error(`${path}.detailScreenshotName must be a safe PNG filename`);
  const zoomScreenshotName = boundedText(raw.zoomScreenshotName, `${path}.zoomScreenshotName`, 120);
  if (!/^[A-Za-z0-9._-]+\.png$/u.test(zoomScreenshotName)) throw new Error(`${path}.zoomScreenshotName must be a safe PNG filename`);
  return {
    routeId: routeReference(raw.routeId, `${path}.routeId`, routeIds),
    taskSelector: boundedText(raw.taskSelector, `${path}.taskSelector`, 500),
    barSelector: boundedText(raw.barSelector, `${path}.barSelector`, 500),
    milestoneSelector: boundedText(raw.milestoneSelector, `${path}.milestoneSelector`, 500),
    ganttSelector: boundedText(raw.ganttSelector, `${path}.ganttSelector`, 500),
    scrollSelector: boundedText(raw.scrollSelector, `${path}.scrollSelector`, 500),
    labelPaneSelector: boundedText(raw.labelPaneSelector, `${path}.labelPaneSelector`, 500),
    scaleSelector: boundedText(raw.scaleSelector, `${path}.scaleSelector`, 500),
    tickSelector: boundedText(raw.tickSelector, `${path}.tickSelector`, 500),
    zoomInButtonName: boundedText(raw.zoomInButtonName, `${path}.zoomInButtonName`, 120),
    secondsScaleLabel: boundedText(raw.secondsScaleLabel, `${path}.secondsScaleLabel`, 40),
    zoomScreenshotName,
    detailSelector: boundedText(raw.detailSelector, `${path}.detailSelector`, 500),
    dialogSelector: boundedText(raw.dialogSelector, `${path}.dialogSelector`, 500),
    dockedSelector: boundedText(raw.dockedSelector, `${path}.dockedSelector`, 500),
    reportSelector: boundedText(raw.reportSelector, `${path}.reportSelector`, 500),
    detailScreenshotName,
  };
}

function projectsWorkflow(value: unknown, path: string, routeIds: ReadonlySet<string>): HwlabRuntimeWebProbeConsoleProjectsWorkflowSpec {
  const raw = record(value, path);
  onlyKeys(raw, ["routeId", "listButtonName", "viewQueryKey"], path);
  return {
    routeId: routeReference(raw.routeId, `${path}.routeId`, routeIds),
    listButtonName: boundedText(raw.listButtonName, `${path}.listButtonName`, 100),
    viewQueryKey: simpleId(text(raw.viewQueryKey, `${path}.viewQueryKey`), `${path}.viewQueryKey`),
  };
}

function hwpodWorkflow(value: unknown, path: string, routeIds: ReadonlySet<string>): HwlabRuntimeWebProbeConsoleHwpodWorkflowSpec {
  const raw = record(value, path);
  onlyKeys(raw, [
    "devicesRouteId", "nodesRouteId", "onboardingRouteId", "listButtonName", "cardButtonName",
    "devicesListSelector", "devicesCardSelector", "nodesListSelector", "pcbTraceSelector", "detailButtonName",
    "detailPathPrefix", "onboardingCommandSelector", "requiredCommandPrefixes", "downloadSelector",
    "forbiddenDownloadSubstrings", "forbiddenInternalSelector",
  ], path);
  return {
    devicesRouteId: routeReference(raw.devicesRouteId, `${path}.devicesRouteId`, routeIds),
    nodesRouteId: routeReference(raw.nodesRouteId, `${path}.nodesRouteId`, routeIds),
    onboardingRouteId: routeReference(raw.onboardingRouteId, `${path}.onboardingRouteId`, routeIds),
    listButtonName: boundedText(raw.listButtonName, `${path}.listButtonName`, 100),
    cardButtonName: boundedText(raw.cardButtonName, `${path}.cardButtonName`, 100),
    devicesListSelector: boundedText(raw.devicesListSelector, `${path}.devicesListSelector`, 500),
    devicesCardSelector: boundedText(raw.devicesCardSelector, `${path}.devicesCardSelector`, 500),
    nodesListSelector: boundedText(raw.nodesListSelector, `${path}.nodesListSelector`, 500),
    pcbTraceSelector: boundedText(raw.pcbTraceSelector, `${path}.pcbTraceSelector`, 500),
    detailButtonName: boundedText(raw.detailButtonName, `${path}.detailButtonName`, 100),
    detailPathPrefix: absolutePath(text(raw.detailPathPrefix, `${path}.detailPathPrefix`), `${path}.detailPathPrefix`),
    onboardingCommandSelector: boundedText(raw.onboardingCommandSelector, `${path}.onboardingCommandSelector`, 500),
    requiredCommandPrefixes: textArray(raw.requiredCommandPrefixes, `${path}.requiredCommandPrefixes`, 1, 20, 300),
    downloadSelector: boundedText(raw.downloadSelector, `${path}.downloadSelector`, 500),
    forbiddenDownloadSubstrings: textArray(raw.forbiddenDownloadSubstrings, `${path}.forbiddenDownloadSubstrings`, 1, 20, 300),
    forbiddenInternalSelector: boundedText(raw.forbiddenInternalSelector, `${path}.forbiddenInternalSelector`, 500),
  };
}

function mdtodoWorkflow(value: unknown, path: string, routeIds: ReadonlySet<string>): HwlabRuntimeWebProbeConsoleMdtodoWorkflowSpec {
  const raw = record(value, path);
  onlyKeys(raw, [
    "routeId", "taskSelector", "taskDeepLinkSegment", "taskDetailSelector", "reportLinkSelector",
    "reportPreviewSelector", "fullscreenButtonSelector", "fullscreenDialogSelector",
  ], path);
  const segment = boundedText(raw.taskDeepLinkSegment, `${path}.taskDeepLinkSegment`, 100);
  if (!segment.startsWith("/")) throw new Error(`${path}.taskDeepLinkSegment must start with /`);
  return {
    routeId: routeReference(raw.routeId, `${path}.routeId`, routeIds),
    taskSelector: boundedText(raw.taskSelector, `${path}.taskSelector`, 500),
    taskDeepLinkSegment: segment,
    taskDetailSelector: boundedText(raw.taskDetailSelector, `${path}.taskDetailSelector`, 500),
    reportLinkSelector: boundedText(raw.reportLinkSelector, `${path}.reportLinkSelector`, 500),
    reportPreviewSelector: boundedText(raw.reportPreviewSelector, `${path}.reportPreviewSelector`, 500),
    fullscreenButtonSelector: boundedText(raw.fullscreenButtonSelector, `${path}.fullscreenButtonSelector`, 500),
    fullscreenDialogSelector: boundedText(raw.fullscreenDialogSelector, `${path}.fullscreenDialogSelector`, 500),
  };
}

function agentObserverWorkflow(value: unknown, path: string, routeIds: ReadonlySet<string>): HwlabRuntimeWebProbeConsoleAgentObserverWorkflowSpec {
  const raw = record(value, path);
  onlyKeys(raw, ["routeId", "eventSourcePath", "phaseSelector", "blockedPhases", "streamSettleMs", "views", "selection"], path);
  const views = array(raw.views, `${path}.views`, 1, 10).map((item, index) => agentView(item, `${path}.views[${index}]`));
  const selection = raw.selection === undefined ? undefined : agentSelection(raw.selection, `${path}.selection`);
  unique(views.map((item) => item.queryValue), `${path}.views[].queryValue`);
  unique([...views.map((item) => item.screenshotName), ...(selection === undefined ? [] : [selection.screenshotName])], `${path}.screenshots`);
  return {
    routeId: routeReference(raw.routeId, `${path}.routeId`, routeIds),
    eventSourcePath: absolutePath(text(raw.eventSourcePath, `${path}.eventSourcePath`), `${path}.eventSourcePath`),
    phaseSelector: boundedText(raw.phaseSelector, `${path}.phaseSelector`, 500),
    blockedPhases: textArray(raw.blockedPhases, `${path}.blockedPhases`, 1, 20, 100),
    streamSettleMs: integer(raw.streamSettleMs, `${path}.streamSettleMs`, 0, 120_000),
    views,
    ...(selection === undefined ? {} : { selection }),
  };
}

function agentSelection(value: unknown, path: string): HwlabRuntimeWebProbeConsoleAgentSelectionSpec {
  const raw = record(value, path);
  onlyKeys(raw, ["selector", "inspectorSelector", "screenshotName"], path);
  const screenshotName = boundedText(raw.screenshotName, `${path}.screenshotName`, 120);
  if (!/^[A-Za-z0-9._-]+\.png$/u.test(screenshotName)) throw new Error(`${path}.screenshotName must be a safe PNG filename`);
  return {
    selector: boundedText(raw.selector, `${path}.selector`, 500),
    inspectorSelector: boundedText(raw.inspectorSelector, `${path}.inspectorSelector`, 500),
    screenshotName,
  };
}

function agentView(value: unknown, path: string): HwlabRuntimeWebProbeConsoleAgentViewSpec {
  const raw = record(value, path);
  onlyKeys(raw, ["buttonName", "queryValue", "screenshotName"], path);
  const screenshotName = boundedText(raw.screenshotName, `${path}.screenshotName`, 120);
  if (!/^[A-Za-z0-9._-]+\.png$/u.test(screenshotName)) throw new Error(`${path}.screenshotName must be a safe PNG filename`);
  return {
    buttonName: boundedText(raw.buttonName, `${path}.buttonName`, 100),
    queryValue: simpleId(text(raw.queryValue, `${path}.queryValue`), `${path}.queryValue`),
    screenshotName,
  };
}

function routeReference(value: unknown, path: string, routeIds: ReadonlySet<string>): string {
  const id = simpleId(text(value, path), path);
  if (!routeIds.has(id)) throw new Error(`${path} references unknown route ${id}`);
  return id;
}

function record(value: unknown, path: string): Record<string, unknown> {
  if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error(`${path} must be an object`);
  return value as Record<string, unknown>;
}

function array(value: unknown, path: string, min: number, max: number): unknown[] {
  if (!Array.isArray(value) || value.length < min || value.length > max) throw new Error(`${path} must contain ${min}-${max} items`);
  return value;
}

function text(value: unknown, path: string): string {
  if (typeof value !== "string" || value.trim().length === 0 || value.includes("\0")) throw new Error(`${path} must be a non-empty string`);
  return value;
}

function boundedText(value: unknown, path: string, max: number): string {
  const result = text(value, path);
  if (result.length > max) throw new Error(`${path} must be at most ${max} chars`);
  return result;
}

function textArray(value: unknown, path: string, min: number, max: number, maxChars: number): readonly string[] {
  return array(value, path, min, max).map((item, index) => boundedText(item, `${path}[${index}]`, maxChars));
}

function simpleId(value: string, path: string): string {
  if (!/^[A-Za-z0-9._-]+$/u.test(value) || value.length > 100) throw new Error(`${path} must be a simple id up to 100 chars`);
  return value;
}

function absolutePath(value: string, path: string): string {
  if (!value.startsWith("/") || value.length > 300) throw new Error(`${path} must be an absolute path up to 300 chars`);
  return value;
}

function integer(value: unknown, path: string, min: number, max: number): number {
  if (!Number.isInteger(value) || (value as number) < min || (value as number) > max) throw new Error(`${path} must be an integer ${min}-${max}`);
  return value as number;
}

function boolean(value: unknown, path: string): boolean {
  if (typeof value !== "boolean") throw new Error(`${path} must be boolean`);
  return value;
}

function onlyKeys(raw: Record<string, unknown>, allowed: readonly string[], path: string): void {
  const allow = new Set(allowed);
  const unknown = Object.keys(raw).filter((key) => !allow.has(key));
  if (unknown.length > 0) throw new Error(`${path} has unsupported fields: ${unknown.join(", ")}`);
}

function unique(values: readonly string[], path: string): void {
  const seen = new Set<string>();
  for (const value of values) {
    if (seen.has(value)) throw new Error(`${path} contains duplicate ${value}`);
    seen.add(value);
  }
}
