"""OpenCV preprocessing pipeline for raster-to-vector conversion."""

import cv2
import numpy as np


def decode_image(raw_bytes: bytes) -> np.ndarray:
    """Decode raw image bytes into a BGR numpy array."""
    buf = np.frombuffer(raw_bytes, dtype=np.uint8)
    img = cv2.imdecode(buf, cv2.IMREAD_COLOR)
    if img is None:
        raise ValueError("Failed to decode image from provided bytes")
    return img


def to_grayscale(img: np.ndarray) -> np.ndarray:
    """Convert BGR image to single-channel grayscale."""
    if len(img.shape) == 2:
        return img
    return cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)


def denoise(
    img: np.ndarray,
    d: int = 9,
    sigma_color: float = 75.0,
    sigma_space: float = 75.0,
) -> np.ndarray:
    """Apply bilateral filter for edge-preserving denoising."""
    return cv2.bilateralFilter(img, d, sigma_color, sigma_space)


def enhance_contrast(
    img: np.ndarray,
    clip_limit: float = 2.0,
    tile_grid_size: tuple[int, int] = (8, 8),
) -> np.ndarray:
    """Apply CLAHE contrast enhancement."""
    clahe = cv2.createCLAHE(clipLimit=clip_limit, tileGridSize=tile_grid_size)
    return clahe.apply(img)


def threshold(
    img: np.ndarray,
    manual_thresh: int | None = None,
) -> np.ndarray:
    """Apply thresholding — Otsu auto by default, manual override if provided."""
    if manual_thresh is not None:
        _, result = cv2.threshold(img, manual_thresh, 255, cv2.THRESH_BINARY)
    else:
        _, result = cv2.threshold(img, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
    return result


def edge_detect(
    img: np.ndarray,
    low: int = 50,
    high: int = 150,
) -> np.ndarray:
    """Apply Canny edge detection."""
    return cv2.Canny(img, low, high)


def morphological_ops(
    img: np.ndarray,
    kernel_size: int = 3,
    dilate_iterations: int = 1,
    erode_iterations: int = 1,
) -> np.ndarray:
    """Apply dilation then erosion (closing-style) to clean up binary image."""
    kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (kernel_size, kernel_size))
    result = cv2.dilate(img, kernel, iterations=dilate_iterations)
    result = cv2.erode(result, kernel, iterations=erode_iterations)
    return result


def preprocess(
    raw_bytes: bytes,
    params: dict | None = None,
) -> np.ndarray:
    """Run the full preprocessing pipeline on raw image bytes.

    Stages: decode → grayscale → denoise → contrast → threshold → morphological ops.
    Edge detection is optional (enabled via params["edge_detect"] = True).

    All stage parameters are tunable via the params dict. Keys:
        denoise_d, denoise_sigma_color, denoise_sigma_space,
        clahe_clip_limit, clahe_tile_grid_size,
        threshold_manual,
        edge_detect (bool), edge_low, edge_high,
        morph_kernel_size, morph_dilate_iterations, morph_erode_iterations
    """
    p = params or {}

    img = decode_image(raw_bytes)
    img = to_grayscale(img)

    img = denoise(
        img,
        d=p.get("denoise_d", 9),
        sigma_color=p.get("denoise_sigma_color", 75.0),
        sigma_space=p.get("denoise_sigma_space", 75.0),
    )

    img = enhance_contrast(
        img,
        clip_limit=p.get("clahe_clip_limit", 2.0),
        tile_grid_size=p.get("clahe_tile_grid_size", (8, 8)),
    )

    img = threshold(
        img,
        manual_thresh=p.get("threshold_manual"),
    )

    if p.get("edge_detect", False):
        img = edge_detect(
            img,
            low=p.get("edge_low", 50),
            high=p.get("edge_high", 150),
        )

    img = morphological_ops(
        img,
        kernel_size=p.get("morph_kernel_size", 3),
        dilate_iterations=p.get("morph_dilate_iterations", 1),
        erode_iterations=p.get("morph_erode_iterations", 1),
    )

    return img
