#!/usr/bin/env python3
"""
Immich Face Integration Module

Integrates with Immich's face recognition system to leverage its existing
face clustering and recognition data for media-downloader files.

Immich uses:
- InsightFace with buffalo_l model (same as media-downloader)
- DBSCAN clustering for face grouping
- 512-dimensional face embeddings
- PostgreSQL for storage

Path mapping:
- Media-downloader: /opt/immich/md/...
- Immich sees: /mnt/media/md/...
"""

import os
import json
from pathlib import Path
from typing import Optional, List, Dict, Any, Tuple
from datetime import datetime
import httpx
from modules.universal_logger import get_logger

logger = get_logger('ImmichFace')


class ImmichFaceIntegration:
    """Interface with Immich's face recognition system."""

    # Path mapping between systems
    LOCAL_BASE = '/opt/immich'
    IMMICH_BASE = '/mnt/media'

    def __init__(self, api_url: str = None, api_key: str = None):
        """
        Initialize Immich face integration.

        Args:
            api_url: Immich API URL (default: http://localhost:2283/api)
            api_key: Immich API key
        """
        self.api_url = (api_url or os.getenv('IMMICH_API_URL', 'http://localhost:2283/api')).rstrip('/')
        self.api_key = api_key or os.getenv('IMMICH_API_KEY', '')
        self._client = None
        self._people_cache = None
        self._people_cache_time = None
        self._cache_ttl = 300  # 5 minutes

    @property
    def is_configured(self) -> bool:
        """Check if Immich integration is properly configured."""
        return bool(self.api_key)

    def _get_client(self) -> httpx.Client:
        """Get or create HTTP client."""
        if self._client is None:
            self._client = httpx.Client(
                base_url=self.api_url,
                headers={
                    'x-api-key': self.api_key,
                    'Accept': 'application/json'
                },
                timeout=30.0
            )
        return self._client

    def _local_to_immich_path(self, local_path: str) -> str:
        """
        Convert local path to Immich's path format.

        Example:
            /opt/immich/md/instagram/user/image.jpg
            -> /mnt/media/md/instagram/user/image.jpg
        """
        return local_path.replace(self.LOCAL_BASE, self.IMMICH_BASE)

    def _immich_to_local_path(self, immich_path: str) -> str:
        """
        Convert Immich's path to local path format.

        Example:
            /mnt/media/md/instagram/user/image.jpg
            -> /opt/immich/md/instagram/user/image.jpg
        """
        return immich_path.replace(self.IMMICH_BASE, self.LOCAL_BASE)

    def test_connection(self) -> Dict[str, Any]:
        """
        Test connection to Immich API.

        Returns:
            Dict with 'success', 'message', and optionally 'server_info'
        """
        if not self.is_configured:
            return {
                'success': False,
                'message': 'Immich API key not configured'
            }

        try:
            client = self._get_client()
            response = client.get('/server/ping')

            if response.status_code == 200:
                # Get server info
                info_response = client.get('/server/version')
                server_info = info_response.json() if info_response.status_code == 200 else {}

                return {
                    'success': True,
                    'message': 'Connected to Immich',
                    'server_info': server_info
                }
            else:
                return {
                    'success': False,
                    'message': f'Immich API returned status {response.status_code}'
                }
        except httpx.ConnectError as e:
            return {
                'success': False,
                'message': f'Cannot connect to Immich at {self.api_url}: {e}'
            }
        except Exception as e:
            return {
                'success': False,
                'message': f'Immich API error: {e}'
            }

    def get_all_people(self, force_refresh: bool = False) -> List[Dict[str, Any]]:
        """
        Get all people/faces from Immich.

        Returns:
            List of people with id, name, thumbnailPath, etc.
        """
        if not self.is_configured:
            return []

        # Check cache
        if not force_refresh and self._people_cache is not None:
            if self._people_cache_time:
                age = (datetime.now() - self._people_cache_time).total_seconds()
                if age < self._cache_ttl:
                    return self._people_cache

        try:
            client = self._get_client()
            response = client.get('/people')

            if response.status_code == 200:
                data = response.json()
                # Immich returns {'people': [...], 'total': N, ...}
                people = data.get('people', data) if isinstance(data, dict) else data

                # Cache the result
                self._people_cache = people
                self._people_cache_time = datetime.now()

                logger.info(f"Fetched {len(people)} people from Immich")
                return people
            else:
                logger.error(f"Failed to get people: {response.status_code}")
                return []
        except Exception as e:
            logger.error(f"Error getting people from Immich: {e}")
            return []

    def get_named_people(self) -> List[Dict[str, Any]]:
        """
        Get only people with names assigned in Immich.

        Returns:
            List of named people
        """
        people = self.get_all_people()
        return [p for p in people if p.get('name')]

    def get_asset_by_path(self, local_path: str) -> Optional[Dict[str, Any]]:
        """
        Find an Immich asset by its file path.

        Args:
            local_path: Local file path (e.g., /opt/immich/md/...)

        Returns:
            Asset dict or None if not found
        """
        if not self.is_configured:
            return None

        immich_path = self._local_to_immich_path(local_path)

        try:
            client = self._get_client()

            # Search by original path
            response = client.post('/search/metadata', json={
                'originalPath': immich_path
            })

            if response.status_code == 200:
                data = response.json()
                assets = data.get('assets', {}).get('items', [])
                if assets:
                    return assets[0]

            return None
        except Exception as e:
            logger.error(f"Error searching asset by path: {e}")
            return None

    def get_faces_for_asset(self, asset_id: str) -> List[Dict[str, Any]]:
        """
        Get all detected faces for an asset.

        Args:
            asset_id: Immich asset ID

        Returns:
            List of face data including person info and bounding boxes
        """
        if not self.is_configured:
            return []

        try:
            client = self._get_client()
            response = client.get(f'/faces', params={'id': asset_id})

            if response.status_code == 200:
                return response.json()
            else:
                logger.warning(f"Failed to get faces for asset {asset_id}: {response.status_code}")
                return []
        except Exception as e:
            logger.error(f"Error getting faces for asset: {e}")
            return []

    def get_faces_for_file(self, local_path: str) -> Dict[str, Any]:
        """
        Get face recognition results for a local file using Immich.

        This is the main method for integration - given a local file path,
        it finds the asset in Immich and returns any detected faces.

        Args:
            local_path: Local file path (e.g., /opt/immich/md/...)

        Returns:
            Dict with:
                - found: bool - whether file exists in Immich
                - faces: list of detected faces with person names
                - asset_id: Immich asset ID if found
        """
        if not self.is_configured:
            return {
                'found': False,
                'error': 'Immich not configured',
                'faces': []
            }

        # Find the asset
        asset = self.get_asset_by_path(local_path)
        if not asset:
            return {
                'found': False,
                'error': 'File not found in Immich',
                'faces': []
            }

        asset_id = asset.get('id')

        # Get faces for the asset
        faces_data = self.get_faces_for_asset(asset_id)

        # Process faces into a more usable format
        faces = []
        for face in faces_data:
            person = face.get('person', {})
            faces.append({
                'face_id': face.get('id'),
                'person_id': person.get('id'),
                'person_name': person.get('name', ''),
                'bounding_box': {
                    'x1': face.get('boundingBoxX1'),
                    'y1': face.get('boundingBoxY1'),
                    'x2': face.get('boundingBoxX2'),
                    'y2': face.get('boundingBoxY2')
                },
                'image_width': face.get('imageWidth'),
                'image_height': face.get('imageHeight')
            })

        # Filter to only named faces
        named_faces = [f for f in faces if f['person_name']]

        return {
            'found': True,
            'asset_id': asset_id,
            'faces': faces,
            'named_faces': named_faces,
            'face_count': len(faces),
            'named_count': len(named_faces)
        }

    def get_person_by_name(self, name: str) -> Optional[Dict[str, Any]]:
        """
        Find a person in Immich by name.

        Args:
            name: Person name to search for

        Returns:
            Person dict or None
        """
        people = self.get_all_people()
        for person in people:
            if person.get('name', '').lower() == name.lower():
                return person
        return None

    def get_person_assets(self, person_id: str, limit: int = 1000) -> List[Dict[str, Any]]:
        """
        Get all assets containing a specific person using search API.

        Args:
            person_id: Immich person ID
            limit: Maximum number of assets to return

        Returns:
            List of assets
        """
        if not self.is_configured:
            return []

        try:
            client = self._get_client()
            # Use the search/metadata endpoint with personIds filter
            response = client.post('/search/metadata', json={
                'personIds': [person_id],
                'size': limit
            })

            if response.status_code == 200:
                data = response.json()
                return data.get('assets', {}).get('items', [])
            else:
                logger.warning(f"Failed to get assets for person {person_id}: {response.status_code}")
                return []
        except Exception as e:
            logger.error(f"Error getting person assets: {e}")
            return []

    def get_statistics(self) -> Dict[str, Any]:
        """
        Get Immich face recognition statistics.

        Returns:
            Dict with total people, named people, etc.
        """
        people = self.get_all_people()
        named = [p for p in people if p.get('name')]

        return {
            'total_people': len(people),
            'named_people': len(named),
            'unnamed_people': len(people) - len(named),
            'people_by_face_count': sorted(
                [{'name': p.get('name', 'Unnamed'), 'count': p.get('faces', 0)}
                 for p in people if p.get('name')],
                key=lambda x: x['count'],
                reverse=True
            )[:20]
        }

    def close(self):
        """Close HTTP client."""
        if self._client:
            self._client.close()
            self._client = None


# Singleton instance
_immich_integration = None


def get_immich_integration(api_url: str = None, api_key: str = None) -> ImmichFaceIntegration:
    """
    Get or create the Immich face integration instance.

    Args:
        api_url: Optional API URL override
        api_key: Optional API key override

    Returns:
        ImmichFaceIntegration instance
    """
    global _immich_integration

    if _immich_integration is None:
        _immich_integration = ImmichFaceIntegration(api_url, api_key)
    elif api_key and api_key != _immich_integration.api_key:
        # Recreate if API key changed
        _immich_integration.close()
        _immich_integration = ImmichFaceIntegration(api_url, api_key)

    return _immich_integration
