from __future__ import annotations

import hashlib
from pathlib import Path

import cv2
import numpy as np

from ocr_pipeline.config import settings


class ImagePreprocessor:
    """Creates an OCR-friendly derivative without mutating detector input images."""

    def __init__(self) -> None:
        self.config = settings.ocr.preprocess
        self.max_dim = self.config.max_dimension

    def load_image(self, path: str | Path) -> np.ndarray:
        image = cv2.imread(str(path))
        if image is None:
            raise ValueError(f"Failed to load image: {path}")
        return image

    def resize_if_needed(self, image: np.ndarray) -> np.ndarray:
        height, width = image.shape[:2]
        if max(height, width) <= self.max_dim:
            return image
        scale = self.max_dim / max(height, width)
        return cv2.resize(
            image, (int(width * scale), int(height * scale)), interpolation=cv2.INTER_AREA
        )

    def deskew(self, image: np.ndarray) -> np.ndarray:
        if not self.config.deskew:
            return image
        gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
        _, foreground = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)
        coords = np.column_stack(np.where(foreground > 0))
        if len(coords) < 20:
            return image
        angle = cv2.minAreaRect(coords)[-1]
        angle = -(90 + angle) if angle < -45 else -angle
        if abs(angle) <= 0.5:
            return image
        height, width = image.shape[:2]
        matrix = cv2.getRotationMatrix2D((width // 2, height // 2), angle, 1.0)
        return cv2.warpAffine(
            image, matrix, (width, height), flags=cv2.INTER_CUBIC, borderMode=cv2.BORDER_REPLICATE
        )

    def denoise(self, image: np.ndarray) -> np.ndarray:
        return (
            cv2.fastNlMeansDenoisingColored(image, None, 10, 10, 7, 21)
            if self.config.denoise
            else image
        )

    def apply_clahe(self, image: np.ndarray) -> np.ndarray:
        if not self.config.clahe:
            return image
        lab = cv2.cvtColor(image, cv2.COLOR_BGR2LAB)
        lightness, a_channel, b_channel = cv2.split(lab)
        lightness = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8)).apply(lightness)
        return cv2.cvtColor(cv2.merge((lightness, a_channel, b_channel)), cv2.COLOR_LAB2BGR)

    def remove_lines(self, image: np.ndarray) -> np.ndarray:
        if not self.config.remove_lines:
            return image
        gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
        _, binary = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)
        horizontal = cv2.morphologyEx(
            binary, cv2.MORPH_OPEN, cv2.getStructuringElement(cv2.MORPH_RECT, (40, 1))
        )
        vertical = cv2.morphologyEx(
            binary, cv2.MORPH_OPEN, cv2.getStructuringElement(cv2.MORPH_RECT, (1, 40))
        )
        mask = cv2.bitwise_or(horizontal, vertical)
        result = image.copy()
        result[mask > 0] = (255, 255, 255)
        return result

    def adaptive_threshold(self, image: np.ndarray) -> np.ndarray:
        if not self.config.adaptive_threshold:
            return image
        gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
        binary = cv2.adaptiveThreshold(
            gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, 11, 2
        )
        return cv2.cvtColor(binary, cv2.COLOR_GRAY2BGR)

    def preprocess_image(self, image: np.ndarray) -> np.ndarray:
        image = self.resize_if_needed(image)
        image = self.deskew(image)
        image = self.denoise(image)
        image = self.apply_clahe(image)
        image = self.remove_lines(image)
        return self.adaptive_threshold(image)

    def preprocess(self, path: str | Path) -> np.ndarray:
        return self.preprocess_image(self.load_image(path))


def compute_image_hash(path: str | Path) -> str:
    hasher = hashlib.sha256()
    with Path(path).open("rb") as handle:
        for chunk in iter(lambda: handle.read(8192), b""):
            hasher.update(chunk)
    return hasher.hexdigest()
