import os
import re
import uuid
import logging
from io import BytesIO

from botocore.exceptions import BotoCoreError, ClientError
from PIL import Image, ImageFile, UnidentifiedImageError

from config.aws_config import get_aws_settings, get_rekognition_client, get_s3_client


ImageFile.LOAD_TRUNCATED_IMAGES = True

logger = logging.getLogger(__name__)


class UnsupportedImageError(ValueError):
    """Raised when an uploaded file cannot be processed as an image."""


class AWSService:
    """Handles S3 uploads and Rekognition calls."""

    ORIGINALS_FOLDER = "originals"
    PROCESSED_FOLDER = "processed"
    MAX_PROCESSED_DIMENSION = 1920
    DEFAULT_JPEG_QUALITY = 70
    TARGET_PROCESSED_BYTES = 1024 * 1024

    def __init__(self):
        settings = get_aws_settings()
        self.bucket_name = settings.s3_bucket
        self.collection_id = settings.collection_id
        self.region_name = settings.region
        self.s3_client = get_s3_client()
        self.rekognition_client = get_rekognition_client()
        self.ensure_bucket_exists()
        self.ensure_collection_exists()

    def ensure_bucket_exists(self):
        """Validate that the configured S3 bucket exists and is reachable."""
        try:
            self.s3_client.head_bucket(Bucket=self.bucket_name)
            logger.info("S3 bucket ready: %s", self.bucket_name)
        except (ClientError, BotoCoreError) as exc:
            logger.exception("Failed to access S3 bucket %s", self.bucket_name)
            raise RuntimeError(
                f"Failed to access S3 bucket '{self.bucket_name}': {exc}"
            ) from exc

    def ensure_collection_exists(self):
        """Create the Rekognition collection if it does not exist yet."""
        try:
            self.rekognition_client.describe_collection(CollectionId=self.collection_id)
            logger.info("Rekognition collection ready: %s", self.collection_id)
        except self.rekognition_client.exceptions.ResourceNotFoundException:
            try:
                self.rekognition_client.create_collection(CollectionId=self.collection_id)
                logger.info("Created Rekognition collection: %s", self.collection_id)
            except (ClientError, BotoCoreError) as exc:
                logger.exception(
                    "Failed to create Rekognition collection %s", self.collection_id
                )
                raise RuntimeError(
                    f"Failed to create Rekognition collection: {exc}"
                ) from exc
        except (ClientError, BotoCoreError) as exc:
            logger.exception(
                "Failed to describe Rekognition collection %s", self.collection_id
            )
            raise RuntimeError(
                f"Failed to access Rekognition collection: {exc}"
            ) from exc

    def upload_file_object(self, file_object, folder_name, original_filename):
        """
        Upload a file-like object to S3.

        For event images, this stores:
        - the original file under `originals/`
        - the processed JPG under `processed/`

        For all other folders, the legacy single-upload behavior is preserved.
        """
        original_filename = original_filename or "image.jpg"
        file_bytes = self._read_file_bytes(file_object)

        if self._should_create_processed_variant(folder_name):
            return self._upload_event_image_variants(file_bytes, original_filename)

        return self._upload_single_image(
            file_bytes=file_bytes,
            folder_name=folder_name,
            original_filename=original_filename,
            content_type=getattr(file_object, "content_type", None),
        )

    def _should_create_processed_variant(self, folder_name):
        normalized_folder = (folder_name or "").strip("/").lower()
        return normalized_folder == "events"

    def _read_file_bytes(self, file_object):
        file_stream = getattr(file_object, "stream", file_object)
        try:
            file_stream.seek(0)
            file_bytes = file_stream.read()
            file_stream.seek(0)
        except Exception as exc:
            raise RuntimeError(f"Failed to read uploaded file: {exc}") from exc

        if not file_bytes:
            raise UnsupportedImageError("Uploaded file is empty.")

        return file_bytes

    def _upload_event_image_variants(self, file_bytes, original_filename):
        original_extension = os.path.splitext(original_filename)[1].lower() or ".jpg"
        original_key = self._generate_s3_key(
            folder_name=self.ORIGINALS_FOLDER,
            original_filename=original_filename,
            extension=original_extension,
        )
        processed_key = self._generate_s3_key(
            folder_name=self.PROCESSED_FOLDER,
            original_filename=original_filename,
            extension=".jpg",
        )

        # ✅ Apply watermark to original image (bottom-left corner)
        original_bytes = self._apply_watermark(file_bytes, original_filename)

        processed_bytes = self._build_processed_image(file_bytes)

        self._upload_bytes_to_s3(
            file_bytes=original_bytes,
            s3_key=original_key,
            content_type=self._detect_original_content_type(original_filename),
        )
        self._upload_bytes_to_s3(
            file_bytes=processed_bytes,
            s3_key=processed_key,
            content_type="image/jpeg",
        )

        return {
            "original_s3_key": original_key,
            "processed_s3_key": processed_key,
            "original_url": self.build_cdn_url(original_key),   # ✅ CDN for gallery/email
            "processed_url": self.build_s3_url(processed_key),  # ✅ S3 direct for Rekognition
            # Backward-compatible aliases.
            "s3_key": original_key,
            "s3_url": self.build_cdn_url(original_key),          # ✅ CDN
        }

    def _apply_watermark(self, file_bytes: bytes, original_filename: str = 'image.jpg') -> bytes:
        """
        Fetch watermark from event_site_assets table (asset_type='watermark')
        and overlay it on bottom-left of the original image.
        Returns original bytes unchanged if watermark missing or fails.
        """
        try:
            import urllib.request
            from models.user_model import get_site_assets

            assets = get_site_assets()
            watermark_url = (assets.get("watermark") or {}).get("image_path")

            if not watermark_url:
                print("[watermark] No watermark URL in database, skipping")
                return file_bytes

            print(f"[watermark] Downloading: {watermark_url}")
            # Use urllib (built-in, no extra install needed)
            with urllib.request.urlopen(watermark_url, timeout=10) as resp:
                wm_bytes = resp.read()

            watermark_img = Image.open(BytesIO(wm_bytes)).convert("RGBA")

            # Open original image
            with Image.open(BytesIO(file_bytes)) as orig:
                if orig.mode != "RGB":
                    orig = orig.convert("RGB")
                orig = orig.copy()

            img_w, img_h = orig.size

            # Scale watermark to 20% of image width
            wm_w = int(img_w * 0.20)
            ratio = wm_w / watermark_img.width
            wm_h = int(watermark_img.height * ratio)
            watermark_img = watermark_img.resize((wm_w, wm_h), Image.Resampling.LANCZOS)

            # Bottom-left position with 20px padding
            x = 20
            y = img_h - wm_h - 20

            # Paste watermark using its alpha channel
            orig_rgba = orig.convert("RGBA")
            orig_rgba.paste(watermark_img, (x, y), watermark_img)

            # Convert back to RGB JPEG
            result = Image.new("RGB", orig.size, (255, 255, 255))
            result.paste(orig_rgba, mask=orig_rgba.getchannel("A"))

            out = BytesIO()
            # Save in original format at maximum quality — no compression
            ext = os.path.splitext(original_filename)[1].lower() or '.jpg'
            fmt = {
                ".png": "PNG", ".jpg": "JPEG", ".jpeg": "JPEG",
                ".webp": "WEBP", ".bmp": "BMP"
            }.get(ext, "JPEG")
            if fmt == "JPEG":
                result.save(out, format="JPEG", quality=100, subsampling=0)
            elif fmt == "PNG":
                result.save(out, format="PNG", compress_level=0)
            else:
                result.save(out, format=fmt)
            print("[watermark] ✅ Watermark applied successfully")
            return out.getvalue()

        except Exception as exc:
            import traceback
            print(f"[watermark] ❌ FAILED: {exc}")
            print(traceback.format_exc())
            return file_bytes

    def _upload_single_image(self, file_bytes, folder_name, original_filename, content_type=None):
        extension = os.path.splitext(original_filename)[1].lower() or ".jpg"
        s3_key = self._generate_s3_key(folder_name, original_filename, extension)
        self._upload_bytes_to_s3(
            file_bytes=file_bytes,
            s3_key=s3_key,
            content_type=content_type or self._detect_original_content_type(original_filename),
        )
        s3_url = self.build_s3_url(s3_key)
        return {
            "s3_key": s3_key,
            "s3_url": s3_url,
            "original_s3_key": s3_key,
            "original_url": s3_url,
            "processed_s3_key": None,
            "processed_url": None,
        }

    def _generate_s3_key(self, folder_name, original_filename, extension):
        safe_name = self.build_safe_name(original_filename)
        unique_name = f"{safe_name or 'image'}-{uuid.uuid4().hex}{extension}"
        return f"{folder_name.rstrip('/')}/{unique_name}"

    def build_safe_name(self, original_filename):
        safe_name = re.sub(
            r"[^a-zA-Z0-9_-]+",
            "-",
            os.path.splitext(original_filename)[0],
        )
        return safe_name or "image"

    def _upload_bytes_to_s3(self, file_bytes, s3_key, content_type):
        try:
            self.s3_client.upload_fileobj(
                Fileobj=BytesIO(file_bytes),
                Bucket=self.bucket_name,
                Key=s3_key,
                ExtraArgs={"ContentType": content_type},
            )
            logger.info("Uploaded S3 object bucket=%s key=%s", self.bucket_name, s3_key)
        except (ClientError, BotoCoreError) as exc:
            logger.exception("S3 upload failed bucket=%s key=%s", self.bucket_name, s3_key)
            raise RuntimeError(f"Failed to upload file to S3: {exc}") from exc

    def _detect_original_content_type(self, original_filename):
        extension = os.path.splitext(original_filename)[1].lower()
        return {
            ".jpg": "image/jpeg",
            ".jpeg": "image/jpeg",
            ".png": "image/png",
            ".webp": "image/webp",
            ".bmp": "image/bmp",
            ".tif": "image/tiff",
            ".tiff": "image/tiff",
            ".gif": "image/gif",
        }.get(extension, "application/octet-stream")

    def _build_processed_image(self, file_bytes):
        try:
            with Image.open(BytesIO(file_bytes)) as image:
                image = self._normalize_image(image)
                image.thumbnail(
                    (self.MAX_PROCESSED_DIMENSION, self.MAX_PROCESSED_DIMENSION),
                    Image.Resampling.LANCZOS,
                )
                return self._encode_optimized_jpeg(image)
        except (UnidentifiedImageError, OSError) as exc:
            raise UnsupportedImageError(
                "Unsupported image format. Please upload JPG, PNG, WEBP, BMP, TIFF, or GIF."
            ) from exc

    def _normalize_image(self, image):
        # Rekognition-friendly JPG output requires a non-alpha RGB image.
        if image.mode in ("RGBA", "LA"):
            background = Image.new("RGB", image.size, (255, 255, 255))
            alpha_channel = image.getchannel("A")
            background.paste(image.convert("RGBA"), mask=alpha_channel)
            return background
        if image.mode == "P":
            image = image.convert("RGBA")
            background = Image.new("RGB", image.size, (255, 255, 255))
            alpha_channel = image.getchannel("A")
            background.paste(image, mask=alpha_channel)
            return background
        if image.mode != "RGB":
            return image.convert("RGB")
        return image

    def _encode_optimized_jpeg(self, image):
        quality_steps = [70, 65, 60, 55, 50, 45, 40]
        best_bytes = None

        for quality in quality_steps:
            buffer = BytesIO()
            image.save(
                buffer,
                format="JPEG",
                quality=quality,
                optimize=True,
                progressive=True,
            )
            candidate = buffer.getvalue()
            best_bytes = candidate
            if len(candidate) <= self.TARGET_PROCESSED_BYTES:
                return candidate

        if best_bytes is not None and len(best_bytes) <= self.TARGET_PROCESSED_BYTES:
            return best_bytes

        resized_image = image.copy()
        while max(resized_image.size) > 640:
            resized_image.thumbnail(
                (max(640, resized_image.size[0] - 160), max(640, resized_image.size[1] - 160)),
                Image.Resampling.LANCZOS,
            )
            buffer = BytesIO()
            resized_image.save(
                buffer,
                format="JPEG",
                quality=45,
                optimize=True,
                progressive=True,
            )
            candidate = buffer.getvalue()
            best_bytes = candidate
            if len(candidate) <= self.TARGET_PROCESSED_BYTES:
                return candidate

        return best_bytes

    def build_s3_url(self, s3_key):
        """Build the direct S3 URL for a stored key."""
        return f"https://{self.bucket_name}.s3.{self.region_name}.amazonaws.com/{s3_key}"

    def build_cdn_url(self, s3_key):
        """Build CDN URL for originals. Falls back to S3 if CDN_DOMAIN not set in .env."""
        cdn_domain = os.getenv("CDN_DOMAIN", "").strip()
        if cdn_domain:
            # CloudFront origin path is /originals so strip that prefix from the key
            key = s3_key.replace("originals/", "", 1)
            return f"{cdn_domain}/{key}"
        return self.build_s3_url(s3_key)

    def event_image_exists(self, original_filename):
        """Check whether an event image with the same original filename likely exists."""
        extension = os.path.splitext(original_filename or "")[1].lower() or ".jpg"
        prefix = f"{self.ORIGINALS_FOLDER}/{self.build_safe_name(original_filename)}-"

        try:
            response = self.s3_client.list_objects_v2(
                Bucket=self.bucket_name,
                Prefix=prefix,
                MaxKeys=50,
            )
        except (ClientError, BotoCoreError) as exc:
            logger.exception(
                "Failed to list S3 objects bucket=%s prefix=%s",
                self.bucket_name,
                prefix,
            )
            raise RuntimeError(f"Failed to check existing S3 object: {exc}") from exc

        for item in response.get("Contents", []):
            key = item.get("Key", "")
            if key.lower().endswith(extension):
                return True

        return False

    def index_user_face(self, s3_key, external_image_id):
        """Index a single face from an S3 image and return the FaceId."""
        self.ensure_collection_exists()
        try:
            response = self.rekognition_client.index_faces(
                CollectionId=self.collection_id,
                Image={
                    "S3Object": {
                        "Bucket": self.bucket_name,
                        "Name": s3_key,
                    }
                },
                ExternalImageId=external_image_id,
                DetectionAttributes=[],
                MaxFaces=1,
                QualityFilter="AUTO",
            )
            logger.info("Indexed face for external_image_id=%s", external_image_id)
        except (ClientError, BotoCoreError) as exc:
            logger.exception("Rekognition index_faces failed for key=%s", s3_key)
            raise RuntimeError(f"Failed to index face with Rekognition: {exc}") from exc

        face_records = response.get("FaceRecords", [])
        if not face_records:
            raise ValueError("No clear face was detected in the registration image.")

        return face_records[0]["Face"]["FaceId"]

    def search_face_by_image(self, s3_key, face_match_threshold=95, max_faces=1):
        """Search the Rekognition collection for the best face match."""
        self.ensure_collection_exists()
        try:
            response = self.rekognition_client.search_faces_by_image(
                CollectionId=self.collection_id,
                Image={
                    "S3Object": {
                        "Bucket": self.bucket_name,
                        "Name": s3_key,
                    }
                },
                FaceMatchThreshold=face_match_threshold,
                MaxFaces=max_faces,
            )
        except self.rekognition_client.exceptions.InvalidParameterException:
            logger.warning("Rekognition search_faces_by_image invalid input for key=%s", s3_key)
            return []
        except (ClientError, BotoCoreError) as exc:
            logger.exception("Rekognition search_faces_by_image failed for key=%s", s3_key)
            raise RuntimeError(f"Failed to search faces with Rekognition: {exc}") from exc

        matches = response.get("FaceMatches", [])
        logger.info("Rekognition search returned %s matches for key=%s", len(matches), s3_key)
        return matches

    def detect_faces(self, s3_key):
        """Detect all faces in an S3 image."""
        try:
            response = self.rekognition_client.detect_faces(
                Image={
                    "S3Object": {
                        "Bucket": self.bucket_name,
                        "Name": s3_key,
                    }
                },
                Attributes=["DEFAULT"],
            )
        except self.rekognition_client.exceptions.InvalidParameterException:
            logger.warning("Rekognition detect_faces invalid input for key=%s", s3_key)
            return []
        except (ClientError, BotoCoreError) as exc:
            logger.exception("Rekognition detect_faces failed for key=%s", s3_key)
            raise RuntimeError(f"Failed to detect faces with Rekognition: {exc}") from exc

        face_details = response.get("FaceDetails", [])
        logger.info("Detected %s faces for key=%s", len(face_details), s3_key)
        return face_details

    def search_face_by_bytes(self, image_bytes, face_match_threshold=95, max_faces=1):
        """Search the Rekognition collection using an in-memory face crop."""
        self.ensure_collection_exists()
        try:
            response = self.rekognition_client.search_faces_by_image(
                CollectionId=self.collection_id,
                Image={"Bytes": image_bytes},
                FaceMatchThreshold=face_match_threshold,
                MaxFaces=max_faces,
            )
        except self.rekognition_client.exceptions.InvalidParameterException:
            logger.warning("Rekognition crop search received invalid bytes payload")
            return []
        except (ClientError, BotoCoreError) as exc:
            logger.exception("Rekognition crop search failed")
            raise RuntimeError(f"Failed to search cropped face with Rekognition: {exc}") from exc

        matches = response.get("FaceMatches", [])
        logger.info("Rekognition crop search returned %s matches", len(matches))
        return matches

    def download_file_bytes(self, s3_key):
        """Download an S3 object into memory."""
        try:
            response = self.s3_client.get_object(Bucket=self.bucket_name, Key=s3_key)
        except (ClientError, BotoCoreError) as exc:
            logger.exception("S3 download failed bucket=%s key=%s", self.bucket_name, s3_key)
            raise RuntimeError(f"Failed to download S3 object: {exc}") from exc

        logger.info("Downloaded S3 object bucket=%s key=%s", self.bucket_name, s3_key)
        return response["Body"].read()