"""
Multi-algorithm image hashing for Verisoo.

Designed to survive social media mangling:
- JPEG recompression (Instagram, Twitter, Facebook)
- Resizing (thumbnails, feed images)
- Format conversion (PNG → JPEG)
- Cropping (platform auto-crops, user crops)
- Color adjustments (filters, auto-enhance)

Strategy: compute multiple hash types, match if ANY is within threshold.
This is how professional systems (TinEye, Content ID) work.
"""

from __future__ import annotations

import hashlib
from io import BytesIO

from PIL import Image

from .models import ImageHashes

# Optional dependency - graceful degradation
try:
    import imagehash

    IMAGEHASH_AVAILABLE = True
except ImportError:
    IMAGEHASH_AVAILABLE = False


def hash_image(image_data: bytes, *, robust: bool = True) -> ImageHashes:
    """
    Compute all hashes for an image.

    Args:
        image_data: Raw image bytes
        robust: If True, compute all hash types for best social media survival.
                If False, only compute required hashes (faster).

    Returns:
        ImageHashes with sha256, phash, dhash, and optionally ahash, colorhash, crop_resistant
    """
    # Cryptographic hash of raw bytes
    sha256 = hashlib.sha256(image_data).hexdigest()

    # Decode image for perceptual hashing
    img = Image.open(BytesIO(image_data))

    if IMAGEHASH_AVAILABLE:
        phash = str(imagehash.phash(img))
        dhash = str(imagehash.dhash(img))

        ahash = None
        colorhash = None
        crop_resistant = None

        if robust:
            ahash = str(imagehash.average_hash(img))
            colorhash = str(imagehash.colorhash(img))
            crop_resistant = _compute_crop_resistant_hash(img)
    else:
        # Fallback without imagehash - less robust but functional
        phash, dhash = _fallback_perceptual_hashes(img)
        ahash = None
        colorhash = None
        crop_resistant = None

    return ImageHashes(
        sha256=sha256,
        phash=phash,
        dhash=dhash,
        ahash=ahash,
        colorhash=colorhash,
        crop_resistant=crop_resistant,
    )


def hash_image_file(path: str, *, robust: bool = True) -> ImageHashes:
    """Hash an image from file path."""
    with open(path, "rb") as f:
        return hash_image(f.read(), robust=robust)


def _compute_crop_resistant_hash(img: Image.Image) -> str:
    """
    Compute hash of center region - survives edge crops.

    Takes center 60% of image and hashes that.
    """
    if not IMAGEHASH_AVAILABLE:
        return ""

    width, height = img.size
    # Crop to center 60%
    left = int(width * 0.2)
    top = int(height * 0.2)
    right = int(width * 0.8)
    bottom = int(height * 0.8)

    center = img.crop((left, top, right, bottom))
    return str(imagehash.phash(center))


def _fallback_perceptual_hashes(img: Image.Image) -> tuple[str, str]:
    """
    Fallback perceptual hashes when imagehash not installed.

    Not truly perceptual, but allows code to run.
    Uses normalized pixel data hashing.
    """
    # Normalize: resize to 32x32, convert to grayscale
    normalized = img.convert("L").resize((32, 32), Image.Resampling.LANCZOS)
    pixel_bytes = normalized.tobytes()

    # Use different portions for phash vs dhash simulation
    phash = hashlib.sha256(pixel_bytes).hexdigest()[:16]
    dhash = hashlib.sha256(pixel_bytes[:512]).hexdigest()[:16]

    return phash, dhash


def perceptual_distance(hash1: str, hash2: str) -> int:
    """
    Compute hamming distance between two perceptual hashes.

    Lower = more similar.
    0 = identical
    1-10 = likely same image, different encoding
    11-20 = possibly same image, significant changes
    >20 = likely different images
    """
    if len(hash1) != len(hash2):
        # Pad shorter hash
        max_len = max(len(hash1), len(hash2))
        hash1 = hash1.zfill(max_len)
        hash2 = hash2.zfill(max_len)

    val1 = int(hash1, 16)
    val2 = int(hash2, 16)
    return bin(val1 ^ val2).count("1")


def is_same_image(
    hashes1: ImageHashes,
    hashes2: ImageHashes,
    *,
    perceptual_threshold: int = 10,
) -> tuple[bool, str]:
    """
    Determine if two sets of hashes represent the same image.

    Uses multi-algorithm matching - succeeds if ANY algorithm matches.

    Returns:
        (is_same, match_type) where match_type is one of:
        - "exact" - SHA-256 identical
        - "phash" - DCT perceptual match
        - "dhash" - Difference hash match
        - "ahash" - Average hash match
        - "colorhash" - Color distribution match
        - "crop_resistant" - Center region match
        - "none" - Not the same image
    """
    # Exact match first
    if hashes1.sha256 == hashes2.sha256:
        return True, "exact"

    # Try perceptual matches
    matches, match_type = hashes1.matches_perceptually(hashes2, perceptual_threshold)
    if matches:
        return True, match_type or "perceptual"

    return False, "none"


def compute_all_distances(hashes1: ImageHashes, hashes2: ImageHashes) -> dict[str, int]:
    """
    Compute hamming distance for all available hash types.

    Useful for debugging and understanding match quality.
    """
    distances = {}

    distances["phash"] = perceptual_distance(hashes1.phash, hashes2.phash)
    distances["dhash"] = perceptual_distance(hashes1.dhash, hashes2.dhash)

    if hashes1.ahash and hashes2.ahash:
        distances["ahash"] = perceptual_distance(hashes1.ahash, hashes2.ahash)
    if hashes1.colorhash and hashes2.colorhash:
        distances["colorhash"] = perceptual_distance(hashes1.colorhash, hashes2.colorhash)
    if hashes1.crop_resistant and hashes2.crop_resistant:
        distances["crop_resistant"] = perceptual_distance(
            hashes1.crop_resistant, hashes2.crop_resistant
        )

    return distances


def extract_exif_metadata(image_data: bytes) -> dict:
    """
    Extract EXIF metadata from image for CaptureMetadata.

    Returns dict with keys matching CaptureMetadata fields where available.
    """
    from datetime import datetime

    from .models import CaptureDevice, GeoLocation

    img = Image.open(BytesIO(image_data))
    exif = img.getexif() if hasattr(img, "getexif") else {}

    result: dict = {
        "width": img.width,
        "height": img.height,
        "mime_type": Image.MIME.get(img.format, f"image/{img.format.lower()}") if img.format else None,
    }

    if not exif:
        return result

    # EXIF tag IDs
    MAKE = 271
    MODEL = 272
    SOFTWARE = 305
    DATETIME_ORIGINAL = 36867
    GPS_INFO = 34853

    # Device info
    make = exif.get(MAKE)
    model = exif.get(MODEL)
    software = exif.get(SOFTWARE)
    if make or model:
        result["device"] = CaptureDevice(
            make=make,
            model=model,
            software=software,
        ).to_dict()

    # Capture time
    datetime_str = exif.get(DATETIME_ORIGINAL)
    if datetime_str:
        try:
            # EXIF format: "2024:03:15 14:32:00"
            captured = datetime.strptime(datetime_str, "%Y:%m:%d %H:%M:%S")
            result["captured_at"] = captured.isoformat()
        except ValueError:
            pass

    # GPS coordinates (simplified - full parsing is complex)
    gps_info = exif.get(GPS_INFO)
    if gps_info and isinstance(gps_info, dict):
        try:
            lat = _parse_gps_coord(gps_info.get(2), gps_info.get(1))  # GPSLatitude, GPSLatitudeRef
            lon = _parse_gps_coord(gps_info.get(4), gps_info.get(3))  # GPSLongitude, GPSLongitudeRef
            if lat is not None and lon is not None:
                result["location"] = GeoLocation(
                    latitude=lat,
                    longitude=lon,
                ).to_dict()
        except (TypeError, ValueError, KeyError):
            pass

    return result


def _parse_gps_coord(coord_tuple: tuple | None, ref: str | None) -> float | None:
    """Parse EXIF GPS coordinate tuple to decimal degrees."""
    if not coord_tuple or not ref:
        return None

    try:
        # coord_tuple is ((deg_num, deg_den), (min_num, min_den), (sec_num, sec_den))
        # or sometimes just (deg, min, sec) as floats
        if isinstance(coord_tuple[0], tuple):
            degrees = coord_tuple[0][0] / coord_tuple[0][1]
            minutes = coord_tuple[1][0] / coord_tuple[1][1]
            seconds = coord_tuple[2][0] / coord_tuple[2][1]
        else:
            degrees, minutes, seconds = coord_tuple

        decimal = degrees + minutes / 60 + seconds / 3600

        if ref in ("S", "W"):
            decimal = -decimal

        return decimal
    except (TypeError, ZeroDivisionError, IndexError):
        return None
