import {
  classifyFromEvidence,
  type KernelStores,
} from "../../../core/src/classify/classifier.js";
import type { ProvenancedEvidence } from "../../../core/src/classify/certainty.js";
import type { ArtifactRef } from "../../../playwright/src/artifacts.js";
import type { ExecutionContext } from "../../../schema/src/records/context.js";
import type { CoverageOutcome } from "../../../schema/src/records/coverage.js";
import type { Finding } from "../../../schema/src/records/finding.js";
import type { Rect } from "../geometry/types.js";

export const VISION_DETECTOR_ID = "vision-advisory" as const;
export const VISION_DETECTOR_VERSION = "1.0.0" as const;
export const VISION_DETECTOR_CLASS = "visual-advisory" as const;

export type VisionProvenance = {
  model: string;
  version: string;
  prompt: string;
};

export type VisionAnnotation = {
  targetFindingId?: string;
  description: string;
  ranking?: number;
  proposedMarkers?: Rect[];
  provenance: VisionProvenance;
};

export type VisionAdapterInput = {
  screenshot: ArtifactRef;
  findings: Finding[];
};

export interface VisionAdapter {
  describe(input: VisionAdapterInput): Promise<VisionAnnotation[]>;
}

export type VisionEvidenceEnvelope = {
  deterministic: unknown;
  visionAnnotations: VisionAnnotation[];
};

export type ApplyVisionAdapterInput = {
  screenshot: ArtifactRef;
  findings: Finding[];
  adapter?: VisionAdapter;
  stores: KernelStores;
  runId: string;
  context: ExecutionContext;
  scope: CoverageOutcome["scope"];
};

export type ApplyVisionAdapterResult = {
  findings: Finding[];
};

function assertNonEmptyString(value: string, field: string): void {
  if (value.trim().length === 0) {
    throw new Error(`${field} must be a non-empty string`);
  }
}

function assertFiniteNumber(value: number, field: string): void {
  if (!Number.isFinite(value)) {
    throw new Error(`${field} must be a finite number`);
  }
}

function assertValidRect(rect: Rect, field: string): void {
  assertFiniteNumber(rect.x, `${field}.x`);
  assertFiniteNumber(rect.y, `${field}.y`);
  assertFiniteNumber(rect.width, `${field}.width`);
  assertFiniteNumber(rect.height, `${field}.height`);
  if (rect.width <= 0 || rect.height <= 0) {
    throw new Error(`${field} width and height must be positive`);
  }
}

function assertValidProvenance(
  provenance: VisionProvenance,
  field: string,
): void {
  assertNonEmptyString(provenance.model, `${field}.model`);
  assertNonEmptyString(provenance.version, `${field}.version`);
  assertNonEmptyString(provenance.prompt, `${field}.prompt`);
}

export function validateVisionAnnotation(
  annotation: VisionAnnotation,
  index: number,
): void {
  const prefix = `annotations[${String(index)}]`;
  assertNonEmptyString(annotation.description, `${prefix}.description`);
  assertValidProvenance(annotation.provenance, `${prefix}.provenance`);

  if (annotation.ranking !== undefined) {
    if (!Number.isFinite(annotation.ranking)) {
      throw new Error(`${prefix}.ranking must be a finite number`);
    }
  }

  if (annotation.targetFindingId !== undefined) {
    assertNonEmptyString(annotation.targetFindingId, `${prefix}.targetFindingId`);
  }

  if (annotation.proposedMarkers !== undefined) {
    annotation.proposedMarkers.forEach((marker, markerIndex) => {
      assertValidRect(marker, `${prefix}.proposedMarkers[${String(markerIndex)}]`);
    });
  }
}

export function readVisionAnnotationsFromEvidence(
  evidence: unknown,
): VisionAnnotation[] | undefined {
  if (typeof evidence !== "object" || evidence === null) {
    return undefined;
  }

  if (Array.isArray(evidence)) {
    for (const item of evidence) {
      const annotations = readVisionAnnotationsFromEvidence(item);
      if (annotations !== undefined) {
        return annotations;
      }
    }
    return undefined;
  }

  const envelope = evidence as Partial<VisionEvidenceEnvelope> & {
    visionAnnotations?: VisionAnnotation[];
    payload?: unknown;
  };

  if (Array.isArray(envelope.visionAnnotations)) {
    return envelope.visionAnnotations;
  }

  if ("payload" in envelope) {
    return readVisionAnnotationsFromEvidence(envelope.payload);
  }

  return undefined;
}

function mergeAnnotationsIntoFinding(
  finding: Finding,
  annotations: VisionAnnotation[],
): Finding {
  if (annotations.length === 0) {
    return finding;
  }

  const evidence: VisionEvidenceEnvelope = {
    deterministic: finding.evidence,
    visionAnnotations: annotations,
  };

  return {
    ...finding,
    evidence,
  };
}

function buildVisionOnlyEvidence(annotation: VisionAnnotation): ProvenancedEvidence[] {
  return [
    {
      truthSource: "inferred",
      payload: {
        description: annotation.description,
        ranking: annotation.ranking,
        proposedMarkers: annotation.proposedMarkers,
        provenance: annotation.provenance,
        visionAnnotations: [annotation],
      },
      visionOnly: true,
    },
  ];
}

function classifyUnlinkedAnnotations(
  annotations: VisionAnnotation[],
  input: ApplyVisionAdapterInput,
): Finding[] {
  if (annotations.length === 0) {
    return [];
  }

  const classified = classifyFromEvidence(
    annotations.map((annotation, index) => ({
      detector: {
        id: VISION_DETECTOR_ID,
        version: VISION_DETECTOR_VERSION,
      },
      class: VISION_DETECTOR_CLASS,
      severity: "info" as const,
      target: {
        kind: "vision",
        canonical: `vision-advisory:${String(index)}`,
      },
      context: input.context,
      summary: annotation.description,
      evidence: buildVisionOnlyEvidence(annotation),
      artifacts: [input.screenshot],
      laneEligibility: "advisory" as const,
      scope: input.scope,
      violation: {
        kind: "vision-advisory",
        description: annotation.description,
        provenance: annotation.provenance,
        deterministicEvidenceLink: null,
      },
      harnessEvents: [],
      coverageEvents: [],
      stores: input.stores,
      runId: input.runId,
    })),
  );

  return classified.findings;
}

export async function applyVisionAdapter(
  input: ApplyVisionAdapterInput,
): Promise<ApplyVisionAdapterResult> {
  assertNonEmptyString(input.runId, "runId");

  if (input.adapter === undefined) {
    return { findings: input.findings };
  }

  const annotations = await input.adapter.describe({
    screenshot: input.screenshot,
    findings: input.findings,
  });

  annotations.forEach((annotation, index) => {
    validateVisionAnnotation(annotation, index);
  });

  const findingIds = new Set(input.findings.map((finding) => finding.id));
  const linkedByFindingId = new Map<string, VisionAnnotation[]>();
  const unlinked: VisionAnnotation[] = [];

  for (const annotation of annotations) {
    if (annotation.targetFindingId === undefined) {
      unlinked.push(annotation);
      continue;
    }

    if (!findingIds.has(annotation.targetFindingId)) {
      throw new Error(
        `unknown targetFindingId ${annotation.targetFindingId} for vision annotation`,
      );
    }

    const existing = linkedByFindingId.get(annotation.targetFindingId) ?? [];
    existing.push(annotation);
    linkedByFindingId.set(annotation.targetFindingId, existing);
  }

  const mergedFindings = input.findings.map((finding) =>
    mergeAnnotationsIntoFinding(finding, linkedByFindingId.get(finding.id) ?? []),
  );

  const advisoryFindings = classifyUnlinkedAnnotations(unlinked, input);

  return {
    findings: [...mergedFindings, ...advisoryFindings],
  };
}
