#!/usr/bin/env python
# app/services/quality/assessment.py
import logging
from pathlib import Path
from typing import Any, Dict, Tuple

import cv2
import numpy as np

logger = logging.getLogger(__name__)

class QualityAssessor:
    """
    Assesses the quality of receipt images for processing suitability.
    """

    def __init__(self, min_quality_score: float = 50.0):
        """
        Initialize the quality assessor.
        
        Args:
            min_quality_score: Minimum acceptable quality score (0-100)
        """
        self.min_quality_score = min_quality_score

    def assess_image(self, image_path: Path) -> Dict[str, Any]:
        """
        Assess the quality of an image.
        
        Args:
            image_path: Path to the image
            
        Returns:
            Dictionary containing quality metrics
        """
        try:
            # Read image
            img = cv2.imread(str(image_path))
            if img is None:
                return {
                    "success": False,
                    "error": f"Failed to read image: {image_path}",
                    "overall_score": 0.0,
                }

            # Convert to grayscale for some metrics
            gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

            # Calculate various quality metrics
            blur_score = self._calculate_blur_score(gray)
            lighting_score = self._calculate_lighting_score(gray)
            contrast_score = self._calculate_contrast_score(gray)
            size_score = self._calculate_size_score(img.shape)

            # Check for potential fold lines
            fold_detected, fold_severity = self._detect_folds(gray)

            # Calculate overall quality score
            overall_score = self._calculate_overall_score({
                "blur": blur_score,
                "lighting": lighting_score,
                "contrast": contrast_score,
                "size": size_score,
                "fold": 100.0 if not fold_detected else (100.0 - fold_severity * 20.0)
            })

            # Create assessment result
            result = {
                "success": True,
                "metrics": {
                    "blur_score": blur_score,
                    "lighting_score": lighting_score,
                    "contrast_score": contrast_score,
                    "size_score": size_score,
                    "fold_detected": fold_detected,
                    "fold_severity": fold_severity if fold_detected else 0.0,
                },
                "overall_score": overall_score,
                "is_acceptable": overall_score >= self.min_quality_score,
                "improvement_suggestions": self._generate_suggestions({
                    "blur": blur_score,
                    "lighting": lighting_score,
                    "contrast": contrast_score,
                    "size": size_score,
                    "fold": fold_detected,
                    "fold_severity": fold_severity if fold_detected else 0.0,
                }),
            }

            return result

        except Exception as e:
            logger.exception(f"Error assessing image quality: {e}")
            return {
                "success": False,
                "error": f"Error assessing image quality: {str(e)}",
                "overall_score": 0.0,
            }

    def _calculate_blur_score(self, gray_img: np.ndarray) -> float:
        """
        Calculate blur score using Laplacian variance.
        Higher variance = less blurry (higher score)
        
        Args:
            gray_img: Grayscale image
            
        Returns:
            Blur score (0-100)
        """
        # Use Laplacian for edge detection
        laplacian = cv2.Laplacian(gray_img, cv2.CV_64F)

        # Calculate variance of Laplacian
        variance = laplacian.var()

        # Map variance to a 0-100 score
        # These thresholds might need adjustment based on your specific requirements
        if variance < 10:
            return 0.0  # Very blurry
        elif variance < 100:
            return (variance - 10) / 90 * 50  # Map 10-100 to 0-50
        elif variance < 1000:
            return 50 + (variance - 100) / 900 * 50  # Map 100-1000 to 50-100
        else:
            return 100.0  # Very sharp

    def _calculate_lighting_score(self, gray_img: np.ndarray) -> float:
        """
        Calculate lighting score based on average brightness and std dev.
        
        Args:
            gray_img: Grayscale image
            
        Returns:
            Lighting score (0-100)
        """
        # Calculate mean brightness
        mean = gray_img.mean()

        # Calculate standard deviation of brightness
        std = gray_img.std()

        # Ideal mean would be around 127 (middle of 0-255)
        # Penalize if too dark or too bright
        mean_score = 100 - abs(mean - 127) / 127 * 100

        # Higher std dev generally means better contrast
        # But we'll cap at 60 for reasonable balance
        std_score = min(std / 60 * 100, 100)

        # Combine scores (weighted)
        return 0.6 * mean_score + 0.4 * std_score

    def _calculate_contrast_score(self, gray_img: np.ndarray) -> float:
        """
        Calculate contrast score.
        
        Args:
            gray_img: Grayscale image
            
        Returns:
            Contrast score (0-100)
        """
        # Calculate histogram
        hist = cv2.calcHist([gray_img], [0], None, [256], [0, 256])

        # Calculate percentage of pixels in each brightness range
        total_pixels = gray_img.shape[0] * gray_img.shape[1]
        dark_pixels = np.sum(hist[:50]) / total_pixels
        mid_pixels = np.sum(hist[50:200]) / total_pixels
        bright_pixels = np.sum(hist[200:]) / total_pixels

        # Ideal: good distribution across ranges with emphasis on mid-range
        # This is a simplified model - real receipts may need different distributions
        score = (
            (0.2 * min(dark_pixels * 500, 100)) +  # Want some dark pixels (text)
            (0.6 * min(mid_pixels * 200, 100)) +   # Want many mid pixels
            (0.2 * min(bright_pixels * 500, 100))  # Want some bright pixels (background)
        )

        return score

    def _calculate_size_score(self, shape: Tuple[int, int, int]) -> float:
        """
        Calculate score based on image dimensions.
        
        Args:
            shape: Image shape (height, width, channels)
            
        Returns:
            Size score (0-100)
        """
        height, width = shape[0], shape[1]

        # Minimum recommended dimensions for good OCR
        min_height, min_width = 800, 600

        # Calculate size score
        if height < min_height or width < min_width:
            # Penalize if below minimum dimensions
            return min(height / min_height, width / min_width) * 100
        else:
            # Full score if dimensions are adequate
            return 100.0

    def _detect_folds(self, gray_img: np.ndarray) -> Tuple[bool, float]:
        """
        Detect potential fold lines in the image.
        
        Args:
            gray_img: Grayscale image
            
        Returns:
            Tuple of (fold_detected, fold_severity)
            fold_severity is a value between 0 and 5, with 5 being the most severe
        """
        # Apply edge detection
        edges = cv2.Canny(gray_img, 50, 150, apertureSize=3)

        # Apply Hough Line Transform to detect straight lines
        lines = cv2.HoughLinesP(
            edges,
            rho=1,
            theta=np.pi/180,
            threshold=100,
            minLineLength=gray_img.shape[1] // 3,  # Look for lines at least 1/3 of image width
            maxLineGap=10
        )

        if lines is None:
            return False, 0.0

        # Check for horizontal or vertical lines that could be folds
        potential_folds = []
        height, width = gray_img.shape

        for line in lines:
            x1, y1, x2, y2 = line[0]
            length = np.sqrt((x2 - x1)**2 + (y2 - y1)**2)
            angle = np.abs(np.arctan2(y2 - y1, x2 - x1) * 180 / np.pi)

            # Check if horizontal (0±10°) or vertical (90±10°)
            is_horizontal = angle < 10 or angle > 170
            is_vertical = abs(angle - 90) < 10

            # Check if length is significant
            is_significant = (is_horizontal and length > width * 0.5) or \
                             (is_vertical and length > height * 0.5)

            if (is_horizontal or is_vertical) and is_significant:
                # Calculate intensity variance along the line
                # This helps determine if it's a fold (sharp brightness change)
                # Simplified implementation for Phase 1
                potential_folds.append({
                    "length": length,
                    "is_horizontal": is_horizontal,
                })

        # Determine if folds are detected and their severity
        if not potential_folds:
            return False, 0.0

        # Severity based on number and length of potential folds
        # This is a simplified metric for Phase 1
        total_len = sum(fold["length"] for fold in potential_folds)
        if is_horizontal:
            severity = min(5.0, total_len / width * 2.5)
        else:
            severity = min(5.0, total_len / height * 2.5)

        return True, severity

    def _calculate_overall_score(self, scores: Dict[str, float]) -> float:
        """
        Calculate overall quality score from individual metrics.
        
        Args:
            scores: Dictionary of individual quality scores
            
        Returns:
            Overall quality score (0-100)
        """
        # Weights for different factors
        weights = {
            "blur": 0.30,
            "lighting": 0.25,
            "contrast": 0.25,
            "size": 0.10,
            "fold": 0.10,
        }

        # Calculate weighted average
        overall = sum(weights[key] * scores[key] for key in weights)

        return overall

    def _generate_suggestions(self, metrics: Dict[str, Any]) -> list:
        """
        Generate improvement suggestions based on metrics.
        
        Args:
            metrics: Dictionary of quality metrics
            
        Returns:
            List of improvement suggestions
        """
        suggestions = []

        # Blur suggestions
        if metrics["blur"] < 60:
            suggestions.append("Hold the camera steady and ensure the receipt is in focus.")

        # Lighting suggestions
        if metrics["lighting"] < 60:
            suggestions.append("Improve lighting conditions and avoid shadows on the receipt.")

        # Contrast suggestions
        if metrics["contrast"] < 60:
            suggestions.append("Ensure good contrast between text and background.")

        # Size suggestions
        if metrics["size"] < 60:
            suggestions.append("Move the camera closer to the receipt for better resolution.")

        # Fold suggestions
        if metrics["fold"]:
            if metrics["fold_severity"] > 3.0:
                suggestions.append("The receipt has severe folds. Try to flatten it before capturing.")
            else:
                suggestions.append("Flatten the receipt to remove fold lines for better processing.")

        return suggestions
