"""Identify BOS forms by QR code before OMRChecker processing."""

from __future__ import annotations

from dataclasses import asdict, dataclass
import json
import re
from pathlib import Path

import cv2


FORM_ID_PATTERN = re.compile(r"^BF\d{6}$")
QR_NOT_READABLE = "QR_NOT_READABLE"
UNKNOWN_FORM_ID = "UNKNOWN_FORM_ID"


@dataclass(frozen=True)
class FormRoute:
    form_id: str
    profile_name: str
    template: Path
    config: Path
    marker: Path
    rotation_clockwise: int

    def as_dict(self) -> dict[str, object]:
        value = asdict(self)
        for key in ("template", "config", "marker"):
            value[key] = str(value[key])
        return value


@dataclass(frozen=True)
class RoutingIssue:
    code: str
    filename: str
    detail: str

    def as_dict(self) -> dict[str, str]:
        return asdict(self)


def load_registry(root: Path) -> dict[str, dict[str, str]]:
    registry_path = root / "profiles" / "form_registry.json"
    return json.loads(registry_path.read_text())


def _decode_qr(image) -> tuple[str, object | None]:
    detector = cv2.QRCodeDetector()
    data, points, _ = detector.detectAndDecode(image)
    return data.strip(), points


def _expected_top_crop(image):
    """Return the top-center band used by normal, correctly oriented BOS forms."""
    height, width = image.shape[:2]
    return image[0 : max(1, int(height * 0.38)), int(width * 0.30) : int(width * 0.70)]


def _rotation_from_points(points, image_shape) -> int:
    """Return clockwise correction needed to put the QR at top-center."""
    if points is None:
        return 0
    height, width = image_shape[:2]
    center = points.reshape(-1, 2).mean(axis=0)
    dx = center[0] - width / 2
    dy = center[1] - height / 2
    # Canonical QR position is top-center. If it is at right/bottom/left,
    # the page was rotated 90/180/270 degrees clockwise respectively.
    if abs(dx / max(width, 1)) > abs(dy / max(height, 1)):
        return 270 if dx > 0 else 90
    return 180 if dy > 0 else 0


def rotate_image_file(path: Path, rotation_clockwise: int) -> None:
    if rotation_clockwise == 0:
        return
    image = cv2.imread(str(path), cv2.IMREAD_GRAYSCALE)
    if image is None:
        raise ValueError(f"Cannot decode image for rotation: {path}")
    if rotation_clockwise == 90:
        image = cv2.rotate(image, cv2.ROTATE_90_CLOCKWISE)
    elif rotation_clockwise == 180:
        image = cv2.rotate(image, cv2.ROTATE_180)
    elif rotation_clockwise == 270:
        image = cv2.rotate(image, cv2.ROTATE_90_COUNTERCLOCKWISE)
    else:
        raise ValueError(f"Unsupported rotation: {rotation_clockwise}")
    if not cv2.imwrite(str(path), image):
        raise OSError(f"Failed to write rotated image: {path}")


def identify_form(path: Path, root: Path) -> tuple[FormRoute | None, RoutingIssue | None]:
    """Read a BF###### QR, normalize orientation, and resolve its form profile."""
    image = cv2.imread(str(path), cv2.IMREAD_GRAYSCALE)
    if image is None:
        return None, RoutingIssue(QR_NOT_READABLE, path.name, "Image cannot be decoded")

    # Fast path: expected top-center QR zone.
    data, points = _decode_qr(_expected_top_crop(image))
    rotation = 0
    if not data:
        # Recovery path: whole-page QR search. QR decoding itself is rotation tolerant;
        # location then tells us how to normalize the page before OMRChecker.
        data, points = _decode_qr(image)
        rotation = _rotation_from_points(points, image.shape)

    if not data or not FORM_ID_PATTERN.fullmatch(data):
        detail = "No readable BF###### QR code found" if not data else f"Invalid BOS form ID: {data!r}"
        return None, RoutingIssue(QR_NOT_READABLE, path.name, detail)

    registry = load_registry(root)
    record = registry.get(data)
    if record is None:
        return None, RoutingIssue(UNKNOWN_FORM_ID, path.name, f"No registered profile for {data}")

    if rotation:
        rotate_image_file(path, rotation)

    route = FormRoute(
        form_id=data,
        profile_name=record["name"],
        template=root / record["template"],
        config=root / record["config"],
        marker=root / record["marker"],
        rotation_clockwise=rotation,
    )
    return route, None
