# pyright: reportOptionalMemberAccess=false, reportInvalidTypeForm=false
"""
Visual comparison functionality for baseline image testing.

Implements visual diff comparison using Pillow and imagehash libraries
for OOXML template screenshot validation and regression testing.

Note: file-level pyright suppressions required because PIL/imagehash are
optional dependencies guarded by runtime availability checks.
"""

from __future__ import annotations

import structlog
import json
from typing import Dict, Any, Optional, Tuple, TYPE_CHECKING
from pathlib import Path
import time

try:
    import numpy as np  # type: ignore[import-not-found]
except ImportError:  # pragma: no cover
    np = None  # type: ignore[assignment]
from dataclasses import dataclass

if TYPE_CHECKING:
    from PIL import Image

try:
    from PIL import Image, ImageChops
except ImportError:
    Image = None  # type: ignore[assignment]
    ImageChops = None  # type: ignore[assignment]

try:
    import imagehash  # type: ignore[import-not-found]
except ImportError:
    imagehash = None  # type: ignore[assignment]

logger = structlog.get_logger(__name__)


@dataclass
class ComparisonResult:
    """Result of visual comparison between two images."""

    similarity_score: float
    are_similar: bool
    difference_percentage: float
    hash_distance: Optional[int]
    structural_similarity: Optional[float]
    pixel_differences: Optional[int]
    diff_image_path: Optional[str]
    metadata: Dict[str, Any]


@dataclass
class ComparisonConfig:
    """Configuration for visual comparison algorithms."""

    similarity_threshold: float = 0.95
    hash_threshold: int = 5
    pixel_tolerance: int = 10
    structural_threshold: float = 0.85
    generate_diff_image: bool = True
    diff_enhancement: bool = True
    ignore_transparency: bool = True


def validate_pillow_available() -> bool:
    """
    Validate that Pillow is available for image processing.

    Returns:
        bool: True if Pillow is available, False otherwise.
    """
    try:
        if Image is None or ImageChops is None:
            logger.error(
                "Pillow (PIL) is not available - install with: pip install Pillow>=10.0.0"
            )
            return False
        return True
    except Exception as e:
        logger.error("Error validating Pillow availability", error=str(e))
        return False


def validate_imagehash_available() -> bool:
    """
    Validate that imagehash is available for perceptual hashing.

    Returns:
        bool: True if imagehash is available, False otherwise.
    """
    try:
        if imagehash is None:
            logger.error(
                "imagehash is not available - install with: pip install imagehash>=4.3.0"
            )
            return False
        return True
    except Exception as e:
        logger.error("Error validating imagehash availability", error=str(e))
        return False


def load_image_safely(image_path: str) -> Optional[Image.Image]:
    """
    Load image file safely with error handling.

    Args:
        image_path: Path to image file.

    Returns:
        PIL Image object if successful, None otherwise.
    """
    try:
        if not validate_pillow_available():
            return None

        image_file = Path(image_path)
        if not image_file.exists():
            logger.warning("Image file does not exist", path=image_path)
            return None

        image = Image.open(image_path)
        logger.debug(
            "Image loaded successfully",
            path=image_path,
            size=image.size,
            mode=image.mode,
        )
        return image

    except Exception as e:
        logger.error("Error loading image", path=image_path, error=str(e))
        return None


def normalize_images(
    image1: Image.Image, image2: Image.Image
) -> Tuple[Image.Image, Image.Image]:
    """
    Normalize two images to same size and mode for comparison.

    Args:
        image1: First image.
        image2: Second image.

    Returns:
        Tuple of normalized images.
    """
    try:
        # Convert to same mode
        if image1.mode != image2.mode:
            if image1.mode == "RGBA" or image2.mode == "RGBA":
                image1 = image1.convert("RGBA")
                image2 = image2.convert("RGBA")
            else:
                image1 = image1.convert("RGB")
                image2 = image2.convert("RGB")

        # Resize to same dimensions (use larger size)
        size1 = image1.size
        size2 = image2.size

        if size1 != size2:
            target_size = (max(size1[0], size2[0]), max(size1[1], size2[1]))

            if size1 != target_size:
                image1 = image1.resize(target_size, Image.Resampling.LANCZOS)
            if size2 != target_size:
                image2 = image2.resize(target_size, Image.Resampling.LANCZOS)

            logger.debug(
                "Images resized for comparison",
                original_sizes=[size1, size2],
                target_size=target_size,
            )

        return image1, image2

    except Exception as e:
        logger.error("Error normalizing images", error=str(e))
        return image1, image2


def calculate_perceptual_hash_distance(
    image1: Image.Image, image2: Image.Image
) -> Optional[int]:
    """
    Calculate perceptual hash distance between two images.

    Args:
        image1: First image.
        image2: Second image.

    Returns:
        Hash distance (lower is more similar), None if error.
    """
    try:
        if not validate_imagehash_available():
            return None

        hash1 = imagehash.average_hash(image1)
        hash2 = imagehash.average_hash(image2)

        distance = abs(hash1 - hash2)
        logger.debug(
            "Perceptual hash calculated",
            hash1=str(hash1),
            hash2=str(hash2),
            distance=distance,
        )

        return distance

    except Exception as e:
        logger.error("Error calculating perceptual hash", error=str(e))
        return None


def calculate_pixel_difference_count(
    image1: Image.Image, image2: Image.Image, tolerance: int = 0
) -> Optional[int]:
    """
    Calculate number of different pixels between two images.

    Args:
        image1: First image.
        image2: Second image.
        tolerance: Pixel value tolerance (0-255).

    Returns:
        Number of different pixels, None if error.
    """
    try:
        # Calculate absolute difference
        diff_image = ImageChops.difference(image1, image2)

        # Convert to numpy for efficient processing
        diff_array = np.array(diff_image)

        # Count pixels that exceed tolerance (sum along channel axis first)
        if diff_array.ndim == 3:
            channel_max = diff_array.max(axis=2)
        else:
            channel_max = diff_array

        if tolerance == 0:
            different_pixels = np.count_nonzero(channel_max)
        else:
            different_pixels = np.count_nonzero(channel_max > tolerance)

        total_pixels = channel_max.size
        logger.debug(
            "Pixel difference calculated",
            different=different_pixels,
            total=total_pixels,
            tolerance=tolerance,
        )

        return int(different_pixels)

    except Exception as e:
        logger.error("Error calculating pixel differences", error=str(e))
        return None


def calculate_structural_similarity(
    image1: Image.Image, image2: Image.Image
) -> Optional[float]:
    """
    Calculate structural similarity between two images.

    Args:
        image1: First image.
        image2: Second image.

    Returns:
        Structural similarity score (0.0-1.0), None if error.
    """
    try:
        # Convert to grayscale for structural analysis
        gray1 = image1.convert("L")
        gray2 = image2.convert("L")

        # Convert to numpy arrays
        array1 = np.array(gray1, dtype=np.float64)
        array2 = np.array(gray2, dtype=np.float64)

        # Calculate means
        mu1 = np.mean(array1)
        mu2 = np.mean(array2)

        # Calculate variances and covariance
        var1 = np.var(array1)
        var2 = np.var(array2)
        cov12 = np.mean((array1 - mu1) * (array2 - mu2))

        # SSIM constants
        c1 = (0.01 * 255) ** 2
        c2 = (0.03 * 255) ** 2

        # Calculate SSIM
        numerator = (2 * mu1 * mu2 + c1) * (2 * cov12 + c2)
        denominator = (mu1**2 + mu2**2 + c1) * (var1 + var2 + c2)

        ssim = numerator / denominator
        logger.debug("Structural similarity calculated", ssim=ssim)

        return float(ssim)

    except Exception as e:
        logger.error("Error calculating structural similarity", error=str(e))
        return None


def generate_diff_image(
    image1: Image.Image, image2: Image.Image, output_path: str, enhance: bool = True
) -> bool:
    """
    Generate visual diff image highlighting differences.

    Args:
        image1: First image.
        image2: Second image.
        output_path: Path to save diff image.
        enhance: Whether to enhance differences for visibility.

    Returns:
        bool: True if diff image generated successfully.
    """
    try:
        logger.info("Generating diff image", output=output_path, enhance=enhance)

        # Calculate difference
        diff_image = ImageChops.difference(image1, image2)

        if enhance:
            # Enhance differences for better visibility
            # Convert to grayscale and apply contrast enhancement
            diff_gray = diff_image.convert("L")

            # Apply threshold to highlight significant differences
            diff_array = np.array(diff_gray)
            threshold = 30  # Minimum difference to highlight
            diff_array[diff_array < threshold] = 0
            diff_array[diff_array >= threshold] = 255

            # Convert back to image
            enhanced_diff = Image.fromarray(diff_array, mode="L")

            # Convert to RGB and colorize differences
            diff_rgb = enhanced_diff.convert("RGB")
            diff_array_rgb = np.array(diff_rgb)

            # Make differences red
            red_mask = diff_array_rgb[:, :, 0] > 0
            diff_array_rgb[red_mask] = [255, 0, 0]  # Red for differences

            final_diff = Image.fromarray(diff_array_rgb)
        else:
            final_diff = diff_image

        # Save diff image
        output_dir = Path(output_path).parent
        output_dir.mkdir(parents=True, exist_ok=True)

        final_diff.save(output_path)
        logger.info("Diff image generated successfully", path=output_path)

        return True

    except Exception as e:
        logger.error("Error generating diff image", error=str(e))
        return False


def compare_images(
    image1_path: str, image2_path: str, config: Optional[ComparisonConfig] = None
) -> ComparisonResult:
    """
    Compare two images using multiple algorithms.

    Args:
        image1_path: Path to first image.
        image2_path: Path to second image.
        config: Comparison configuration.

    Returns:
        ComparisonResult with detailed analysis.
    """
    try:
        logger.info("Starting image comparison", image1=image1_path, image2=image2_path)

        if config is None:
            config = ComparisonConfig()

        # Load images
        image1 = load_image_safely(image1_path)
        image2 = load_image_safely(image2_path)

        if image1 is None or image2 is None:
            return ComparisonResult(
                similarity_score=0.0,
                are_similar=False,
                difference_percentage=100.0,
                hash_distance=None,
                structural_similarity=None,
                pixel_differences=None,
                diff_image_path=None,
                metadata={"error": "Failed to load images"},
            )

        # Normalize images
        image1, image2 = normalize_images(image1, image2)

        # Calculate various similarity metrics
        hash_distance = calculate_perceptual_hash_distance(image1, image2)
        pixel_differences = calculate_pixel_difference_count(
            image1, image2, tolerance=config.pixel_tolerance
        )
        structural_similarity = calculate_structural_similarity(image1, image2)

        # Calculate overall similarity score
        similarity_scores = []

        if hash_distance is not None:
            # Convert hash distance to similarity (0-1)
            hash_similarity = max(
                0, 1 - (hash_distance / 64)
            )  # 64 is max hash distance
            similarity_scores.append(hash_similarity)

        if pixel_differences is not None:
            total_pixels = image1.size[0] * image1.size[1] * len(image1.getbands())
            pixel_similarity = 1 - (pixel_differences / total_pixels)
            similarity_scores.append(pixel_similarity)

        if structural_similarity is not None:
            similarity_scores.append(structural_similarity)

        # Overall similarity is average of available metrics
        overall_similarity = np.mean(similarity_scores) if similarity_scores else 0.0

        # Determine if images are similar
        are_similar = overall_similarity >= config.similarity_threshold

        if hash_distance is not None:
            are_similar = are_similar and hash_distance <= config.hash_threshold

        if structural_similarity is not None:
            are_similar = (
                are_similar and structural_similarity >= config.structural_threshold
            )

        # Calculate difference percentage
        difference_percentage = (1 - overall_similarity) * 100

        # Generate diff image if requested
        diff_image_path = None
        if config.generate_diff_image:
            diff_dir = Path(image1_path).parent / "diffs"
            diff_filename = (
                f"diff_{Path(image1_path).stem}_{Path(image2_path).stem}.png"
            )
            diff_image_path = str(diff_dir / diff_filename)

            if not generate_diff_image(
                image1, image2, diff_image_path, config.diff_enhancement
            ):
                diff_image_path = None

        result = ComparisonResult(
            similarity_score=float(overall_similarity),
            are_similar=bool(are_similar),
            difference_percentage=float(difference_percentage),
            hash_distance=hash_distance,
            structural_similarity=structural_similarity,
            pixel_differences=pixel_differences,
            diff_image_path=diff_image_path,
            metadata={
                "image1_size": image1.size,
                "image2_size": image2.size,
                "image1_mode": image1.mode,
                "image2_mode": image2.mode,
                "config": {
                    "similarity_threshold": config.similarity_threshold,
                    "hash_threshold": config.hash_threshold,
                    "pixel_tolerance": config.pixel_tolerance,
                    "structural_threshold": config.structural_threshold,
                },
            },
        )

        logger.info(
            "Image comparison completed",
            similarity=overall_similarity,
            similar=are_similar,
            difference_pct=difference_percentage,
        )

        return result

    except Exception as e:
        logger.error("Error during image comparison", error=str(e))
        return ComparisonResult(
            similarity_score=0.0,
            are_similar=False,
            difference_percentage=100.0,
            hash_distance=None,
            structural_similarity=None,
            pixel_differences=None,
            diff_image_path=None,
            metadata={"error": str(e)},
        )


def comprehensive_comparison(
    baseline_path: str, current_path: str, output_dir: str
) -> Dict[str, Any]:
    """
    Perform comprehensive comparison with detailed analysis.

    Args:
        baseline_path: Path to baseline image.
        current_path: Path to current image.
        output_dir: Directory for output files.

    Returns:
        Dictionary with comprehensive comparison results.
    """
    try:
        logger.info(
            "Starting comprehensive comparison",
            baseline=baseline_path,
            current=current_path,
        )

        output_path = Path(output_dir)
        output_path.mkdir(parents=True, exist_ok=True)

        # Multiple comparison configurations
        configs = {
            "strict": ComparisonConfig(
                similarity_threshold=0.98, hash_threshold=2, pixel_tolerance=5
            ),
            "normal": ComparisonConfig(
                similarity_threshold=0.95, hash_threshold=5, pixel_tolerance=10
            ),
            "relaxed": ComparisonConfig(
                similarity_threshold=0.90, hash_threshold=10, pixel_tolerance=20
            ),
        }

        results = {}

        for config_name, config in configs.items():
            logger.debug("Running comparison with config", config=config_name)

            # Set unique diff image path for each config
            diff_filename = f"diff_{config_name}_{Path(baseline_path).stem}.png"
            config.generate_diff_image = True

            # Temporarily modify comparison to save diff in correct location
            result = compare_images(baseline_path, current_path, config)

            # Move diff image to output directory if generated
            if result.diff_image_path and Path(result.diff_image_path).exists():
                new_diff_path = output_path / diff_filename
                Path(result.diff_image_path).rename(new_diff_path)
                result.diff_image_path = str(new_diff_path)

            results[config_name] = {
                "similarity_score": result.similarity_score,
                "are_similar": result.are_similar,
                "difference_percentage": result.difference_percentage,
                "hash_distance": result.hash_distance,
                "structural_similarity": result.structural_similarity,
                "pixel_differences": result.pixel_differences,
                "diff_image_path": result.diff_image_path,
                "metadata": result.metadata,
            }

        # Generate summary report
        summary = {
            "baseline_image": baseline_path,
            "current_image": current_path,
            "output_directory": str(output_path),
            "comparison_results": results,
            "overall_assessment": {
                "passed_strict": results["strict"]["are_similar"],
                "passed_normal": results["normal"]["are_similar"],
                "passed_relaxed": results["relaxed"]["are_similar"],
                "recommended_action": "approve"
                if results["normal"]["are_similar"]
                else "review",
            },
            "timestamp": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
        }

        # Save comprehensive report
        report_path = output_path / "comparison_report.json"
        with open(report_path, "w") as f:
            json.dump(summary, f, indent=2)

        logger.info(
            "Comprehensive comparison completed",
            report=str(report_path),
            passed_normal=results["normal"]["are_similar"],
        )

        return summary

    except Exception as e:
        logger.error("Error in comprehensive comparison", error=str(e))
        return {
            "error": str(e),
            "baseline_image": baseline_path,
            "current_image": current_path,
            "timestamp": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
        }


def validate_comparison_environment() -> bool:
    """
    Validate that all required dependencies are available.

    Returns:
        bool: True if environment is ready for comparison.
    """
    try:
        logger.info("Validating visual comparison environment")

        checks = {
            "pillow": validate_pillow_available(),
            "imagehash": validate_imagehash_available(),
            "numpy": True,  # numpy is always available in this context
        }

        all_available = all(checks.values())

        logger.info(
            "Environment validation completed",
            pillow=checks["pillow"],
            imagehash=checks["imagehash"],
            numpy=checks["numpy"],
            ready=all_available,
        )

        return all_available

    except Exception as e:
        logger.error("Error validating comparison environment", error=str(e))
        return False
