"""Image preprocessing for OCR.

Phase 1 implements the "always-on" steps that work for both clean PDF scans
and reasonable phone photos:

  - resize to a reasonable max side (PaddleOCR runs faster on smaller inputs)
  - convert to grayscale for analysis (kept as 3-channel BGR for paddle)
  - denoise (Non-Local Means, gentle)
  - deskew via Hough line angle estimate
  - optional adaptive threshold for low-quality phone photos

Phase 2 will add document-corner detection + perspective transform + dewarping
for tilted phone shots; those live in `document_detect.py` (added later).
"""

from __future__ import annotations

from dataclasses import dataclass

import cv2
import numpy as np

from ocr_sprint.pipeline.ingest import NDArrayU8


@dataclass(frozen=True)
class PreprocessConfig:
    """Tunable knobs for the preprocessing pipeline."""

    max_side: int = 2200
    denoise: bool = True
    deskew: bool = True
    adaptive_threshold: bool = False


def _resize_max_side(img: NDArrayU8, max_side: int) -> NDArrayU8:
    h, w = img.shape[:2]
    longest = max(h, w)
    if longest <= max_side:
        return img
    scale = max_side / longest
    new_w, new_h = round(w * scale), round(h * scale)
    return cv2.resize(img, (new_w, new_h), interpolation=cv2.INTER_AREA)


def _estimate_skew_angle(gray: NDArrayU8) -> float:
    """Estimate skew using Canny + Hough; returns angle in degrees within [-15, 15]."""
    edges = cv2.Canny(gray, 50, 150, apertureSize=3)
    lines = cv2.HoughLines(edges, 1, np.pi / 360, threshold=200)
    if lines is None or len(lines) == 0:
        return 0.0
    angles: list[float] = []
    for line in lines[:200]:
        rho, theta = line[0]
        del rho
        # convert to angle relative to horizontal (degrees)
        angle = (theta * 180.0 / np.pi) - 90.0
        # only keep nearly-horizontal lines (within ±15°)
        if -15.0 < angle < 15.0:
            angles.append(angle)
    if not angles:
        return 0.0
    return float(np.median(angles))


def _rotate(img: NDArrayU8, angle_deg: float) -> NDArrayU8:
    if abs(angle_deg) < 0.1:
        return img
    h, w = img.shape[:2]
    center = (w / 2, h / 2)
    matrix = cv2.getRotationMatrix2D(center, angle_deg, 1.0)
    return cv2.warpAffine(
        img,
        matrix,
        (w, h),
        flags=cv2.INTER_CUBIC,
        borderMode=cv2.BORDER_REPLICATE,
    )


def preprocess(img: NDArrayU8, cfg: PreprocessConfig | None = None) -> NDArrayU8:
    """Run preprocessing and return a clean BGR uint8 image suitable for OCR."""
    if cfg is None:
        cfg = PreprocessConfig()

    out = _resize_max_side(img, cfg.max_side)

    if cfg.deskew:
        gray = cv2.cvtColor(out, cv2.COLOR_BGR2GRAY)
        angle = _estimate_skew_angle(gray)
        out = _rotate(out, -angle)

    if cfg.denoise:
        out = cv2.fastNlMeansDenoisingColored(out, None, 5, 5, 7, 21)

    if cfg.adaptive_threshold:
        gray = cv2.cvtColor(out, cv2.COLOR_BGR2GRAY)
        binarized = cv2.adaptiveThreshold(
            gray,
            255,
            cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
            cv2.THRESH_BINARY,
            blockSize=31,
            C=15,
        )
        out = cv2.cvtColor(binarized, cv2.COLOR_GRAY2BGR)

    return out
