"""Photo import and management services."""

from __future__ import annotations

import hashlib
import os
from pathlib import Path
from datetime import datetime, date
from typing import Callable, Optional, Tuple, Union

from PIL import Image
from sqlalchemy.orm import Session

from backend.config import SUPPORTED_IMAGE_FORMATS, SUPPORTED_VIDEO_FORMATS
from backend.db.models import Photo


def validate_date_taken(date_taken: Optional[Union[date, datetime]]) -> Optional[date]:
    """Validate and normalize date_taken value.
    
    Ensures the date is:
    - A valid date object (not datetime, not invalid type)
    - Not in the future
    - Not too old (before 1900)
    
    Returns:
        Valid date object or None if invalid
    """
    if date_taken is None:
        return None
    
    try:
        # Convert datetime to date if needed
        if isinstance(date_taken, datetime):
            date_taken = date_taken.date()
        elif not isinstance(date_taken, date):
            # Invalid type
            return None
        
        # Validate date is reasonable
        if date_taken > date.today():
            # Date in the future is invalid
            return None
        elif date_taken < date(1900, 1, 1):
            # Date too old is likely invalid
            return None
        
        return date_taken
    except (ValueError, TypeError, AttributeError) as e:
        # If any validation fails, return None
        import logging
        logger = logging.getLogger(__name__)
        logger.warning(f"Invalid date_taken value: {date_taken}, error: {e}")
        return None


def extract_exif_date(image_path: str) -> Optional[date]:
    """Extract date taken from photo EXIF data - returns Date (not DateTime) to match desktop schema.
    
    Tries multiple methods to extract EXIF date:
    1. PIL's getexif() (modern method)
    2. PIL's _getexif() (deprecated but sometimes more reliable)
    3. Access EXIF IFD directly if available
    """
    try:
        with Image.open(image_path) as image:
            exifdata = None
            
            # Try modern getexif() first
            try:
                exifdata = image.getexif()
            except Exception:
                pass
            
            # If getexif() didn't work or returned empty, try deprecated _getexif()
            if not exifdata or len(exifdata) == 0:
                try:
                    if hasattr(image, '_getexif'):
                        exifdata = image._getexif()
                except Exception:
                    pass
            
            if not exifdata:
                return None
            
            # Look for date taken in EXIF tags
            # Priority: DateTimeOriginal (when photo was taken) > DateTimeDigitized > DateTime (file modification)
            date_tags = [
                36867,  # DateTimeOriginal - when photo was actually taken (highest priority)
                36868,  # DateTimeDigitized - when photo was digitized
                306,  # DateTime - file modification date (lowest priority)
            ]
            
            # Try direct access first
            for tag_id in date_tags:
                try:
                    if tag_id in exifdata:
                        date_str = exifdata[tag_id]
                        if date_str:
                            # Parse EXIF date format (YYYY:MM:DD HH:MM:SS)
                            try:
                                dt = datetime.strptime(date_str, "%Y:%m:%d %H:%M:%S")
                                extracted_date = dt.date()
                                # Validate date before returning (reject future dates)
                                if extracted_date > date.today() or extracted_date < date(1900, 1, 1):
                                    continue  # Skip invalid dates
                                return extracted_date
                            except ValueError:
                                # Try alternative format
                                try:
                                    dt = datetime.strptime(date_str, "%Y-%m-%d %H:%M:%S")
                                    extracted_date = dt.date()
                                    # Validate date before returning (reject future dates)
                                    if extracted_date > date.today() or extracted_date < date(1900, 1, 1):
                                        continue  # Skip invalid dates
                                    return extracted_date
                                except ValueError:
                                    continue
                except (KeyError, TypeError):
                    continue
            
            # Try accessing EXIF IFD directly if available (for tags in EXIF IFD like DateTimeOriginal)
            try:
                if hasattr(exifdata, 'get_ifd'):
                    # EXIF IFD is at offset 0x8769
                    exif_ifd = exifdata.get_ifd(0x8769)
                    if exif_ifd:
                        for tag_id in date_tags:
                            if tag_id in exif_ifd:
                                date_str = exif_ifd[tag_id]
                                if date_str:
                                    try:
                                        dt = datetime.strptime(date_str, "%Y:%m:%d %H:%M:%S")
                                        extracted_date = dt.date()
                                        # Validate date before returning (reject future dates)
                                        if extracted_date > date.today() or extracted_date < date(1900, 1, 1):
                                            continue  # Skip invalid dates
                                        return extracted_date
                                    except ValueError:
                                        try:
                                            dt = datetime.strptime(date_str, "%Y-%m-%d %H:%M:%S")
                                            extracted_date = dt.date()
                                            # Validate date before returning (reject future dates)
                                            if extracted_date > date.today() or extracted_date < date(1900, 1, 1):
                                                continue  # Skip invalid dates
                                            return extracted_date
                                        except ValueError:
                                            continue
            except Exception:
                pass
            
    except Exception as e:
        # Log error for debugging (but don't fail the import)
        import logging
        logger = logging.getLogger(__name__)
        logger.debug(f"Failed to extract EXIF date from {image_path}: {e}")
    
    return None


def calculate_file_hash(file_path: str) -> str:
    """Calculate SHA256 hash of file content.
    
    Args:
        file_path: Path to the file to hash
        
    Returns:
        Hexadecimal string representation of SHA256 hash
    """
    sha256_hash = hashlib.sha256()
    try:
        with open(file_path, "rb") as f:
            # Read file in chunks to handle large files efficiently
            for byte_block in iter(lambda: f.read(4096), b""):
                sha256_hash.update(byte_block)
        return sha256_hash.hexdigest()
    except Exception as e:
        # Log error for debugging
        import logging
        logger = logging.getLogger(__name__)
        logger.error(f"Failed to calculate hash for {file_path}: {e}")
        raise


def extract_video_date(video_path: str) -> Optional[date]:
    """Extract date taken from video metadata.
    
    Tries in order:
    1. Video metadata date (using ffprobe if available)
    2. File modification time (as fallback)
    
    Returns:
        Date object or None if no date can be determined
    """
    # Try to extract date from video metadata using ffprobe
    try:
        import subprocess
        import json
        
        # Use ffprobe to get video metadata
        result = subprocess.run(
            [
                "ffprobe",
                "-v", "quiet",
                "-print_format", "json",
                "-show_format",
                video_path
            ],
            capture_output=True,
            text=True,
            timeout=10
        )
        
        if result.returncode == 0:
            metadata = json.loads(result.stdout)
            format_info = metadata.get("format", {})
            
            # Try common date tags in video metadata
            date_tags = [
                "creation_time",  # Common in MP4/MOV
                "date",  # Alternative tag
                "com.apple.quicktime.creationdate",  # QuickTime specific
            ]
            
            for tag in date_tags:
                date_str = format_info.get("tags", {}).get(tag)
                if date_str:
                    try:
                        # Try ISO format first (2023-12-25T10:30:00)
                        if "T" in date_str:
                            dt = datetime.fromisoformat(date_str.replace("Z", "+00:00"))
                        else:
                            # Try other common formats
                            dt = datetime.strptime(date_str, "%Y-%m-%d %H:%M:%S")
                        extracted_date = dt.date()
                        # Validate date before returning (reject future dates)
                        if extracted_date > date.today() or extracted_date < date(1900, 1, 1):
                            continue  # Skip invalid dates
                        return extracted_date
                    except (ValueError, AttributeError):
                        continue
    except (subprocess.TimeoutExpired, FileNotFoundError, json.JSONDecodeError, Exception) as e:
        # ffprobe not available or failed - fall through to file modification time
        import logging
        logger = logging.getLogger(__name__)
        logger.debug(f"Failed to extract video metadata from {video_path}: {e}")
    
    # Fallback to file modification time
    try:
        if os.path.exists(video_path):
            mtime = os.path.getmtime(video_path)
            mtime_date = datetime.fromtimestamp(mtime).date()
            # Validate date before returning (reject future dates)
            if mtime_date > date.today() or mtime_date < date(1900, 1, 1):
                return None  # Skip invalid dates
            return mtime_date
    except Exception as e:
        # Log error for debugging (but don't fail the import)
        import logging
        logger = logging.getLogger(__name__)
        logger.debug(f"Failed to get file modification time from {video_path}: {e}")
    
    return None


def extract_photo_date(image_path: str) -> Optional[date]:
    """Extract date taken from photo with fallback to file modification time.
    
    Tries in order:
    1. EXIF date tags (DateTimeOriginal, DateTimeDigitized, DateTime)
    2. File modification time (as fallback)
    
    Returns:
        Date object or None if no date can be determined
    """
    # First try EXIF date extraction
    date_taken = extract_exif_date(image_path)
    if date_taken:
        return date_taken
    
    # Fallback to file modification time
    try:
        if os.path.exists(image_path):
            mtime = os.path.getmtime(image_path)
            mtime_date = datetime.fromtimestamp(mtime).date()
            # Validate date before returning (reject future dates)
            if mtime_date > date.today() or mtime_date < date(1900, 1, 1):
                return None  # Skip invalid dates
            return mtime_date
    except Exception as e:
        # Log error for debugging (but don't fail the import)
        import logging
        logger = logging.getLogger(__name__)
        logger.debug(f"Failed to get file modification time from {image_path}: {e}")
    
    return None


def find_photos_in_folder(folder_path: str, recursive: bool = True) -> list[str]:
    """Find all photo and video files in a folder.
    
    Returns both image and video files. Videos are scanned but not processed for faces.
    """
    folder_path = os.path.abspath(folder_path)
    if not os.path.isdir(folder_path):
        return []

    found_photos = []
    # Combine image and video formats
    supported_formats = SUPPORTED_IMAGE_FORMATS | SUPPORTED_VIDEO_FORMATS

    if recursive:
        for root, _dirs, files in os.walk(folder_path):
            for file in files:
                file_ext = Path(file).suffix.lower()
                if file_ext in supported_formats:
                    photo_path = os.path.join(root, file)
                    found_photos.append(photo_path)
    else:
        for file in os.listdir(folder_path):
            file_ext = Path(file).suffix.lower()
            if file_ext in supported_formats:
                photo_path = os.path.join(folder_path, file)
                if os.path.isfile(photo_path):
                    found_photos.append(photo_path)

    return found_photos


def import_photo_from_path(
    db: Session, photo_path: str, update_progress: Optional[Callable[[int, int, str], None]] = None
) -> Tuple[Optional[Photo], bool]:
    """Import a single photo or video from file path into database.

    Returns:
        Tuple of (Photo instance or None, is_new: bool)
    """
    photo_path = os.path.abspath(photo_path)
    filename = os.path.basename(photo_path)
    file_ext = Path(photo_path).suffix.lower()
    
    # Determine media type
    if file_ext in SUPPORTED_VIDEO_FORMATS:
        media_type = "video"
    else:
        media_type = "image"

    # Calculate file hash for duplicate detection
    try:
        file_hash = calculate_file_hash(photo_path)
    except Exception as e:
        # If hash calculation fails, we can't proceed
        import logging
        logger = logging.getLogger(__name__)
        logger.error(f"Failed to calculate hash for {photo_path}: {e}")
        raise

    # Check if photo already exists by hash (primary duplicate check)
    existing = db.query(Photo).filter(Photo.file_hash == file_hash).first()
    if existing:
        # If existing photo doesn't have date_taken, try to update it
        if existing.date_taken is None:
            if media_type == "video":
                date_taken = extract_video_date(photo_path)
            else:
                date_taken = extract_photo_date(photo_path)
            # Validate date_taken before setting
            date_taken = validate_date_taken(date_taken)
            if date_taken:
                existing.date_taken = date_taken
                db.commit()
                db.refresh(existing)
        return existing, False

    # Also check by path as fallback (in case hash wasn't set for some reason)
    existing_by_path = db.query(Photo).filter(Photo.path == photo_path).first()
    if existing_by_path:
        # Update hash if missing
        if not existing_by_path.file_hash:
            existing_by_path.file_hash = file_hash
            db.commit()
            db.refresh(existing_by_path)
        # If existing photo doesn't have date_taken, try to update it
        if existing_by_path.date_taken is None:
            if media_type == "video":
                date_taken = extract_video_date(photo_path)
            else:
                date_taken = extract_photo_date(photo_path)
            # Validate date_taken before setting
            date_taken = validate_date_taken(date_taken)
            if date_taken:
                existing_by_path.date_taken = date_taken
                db.commit()
                db.refresh(existing_by_path)
        return existing_by_path, False

    # Extract date taken with fallback to file modification time
    if media_type == "video":
        date_taken = extract_video_date(photo_path)
    else:
        date_taken = extract_photo_date(photo_path)

    # Validate date_taken - ensure it's a valid date object or None
    # This prevents corrupted date data from being saved
    date_taken = validate_date_taken(date_taken)

    # For videos, mark as processed immediately (we don't process videos for faces)
    # For images, start as unprocessed
    processed = media_type == "video"

    # Explicitly set date_added to current UTC time to ensure it's always valid
    date_added = datetime.utcnow()

    # Create new photo record with file_hash and media_type
    photo = Photo(
        path=photo_path,
        filename=filename,
        date_added=date_added,  # Explicitly set to ensure valid DateTime
        date_taken=date_taken,  # Validated date or None
        processed=processed,
        file_hash=file_hash,
        media_type=media_type,
    )

    db.add(photo)
    db.commit()
    db.refresh(photo)

    return photo, True


def import_photos_from_folder(
    db: Session,
    folder_path: str,
    recursive: bool = True,
    update_progress: Optional[Callable[[int, int, str], None]] = None,
) -> Tuple[int, int]:
    """Import all photos from a folder.

    Args:
        db: Database session
        folder_path: Path to folder to scan
        recursive: Whether to scan subdirectories
        update_progress: Optional callback(processed, total, current_file)

    Returns:
        Tuple of (added_count, existing_count)
    """
    found_photos = find_photos_in_folder(folder_path, recursive)
    total = len(found_photos)

    if total == 0:
        return 0, 0

    added_count = 0
    existing_count = 0

    for idx, photo_path in enumerate(found_photos, 1):
        try:
            photo, is_new = import_photo_from_path(db, photo_path)
            if is_new:
                added_count += 1
            else:
                existing_count += 1

            if update_progress:
                update_progress(idx, total, os.path.basename(photo_path))
        except Exception:
            # Log error but continue
            if update_progress:
                update_progress(idx, total, f"Error: {os.path.basename(photo_path)}")

    return added_count, existing_count

