import base64
import hashlib
import mimetypes
import pathlib
import typing

import cv2
import cv2.data
import imagehash
import numpy
import PIL.ExifTags
import PIL.Image
import PIL.ImageChops
import pytesseract

from chitragupt.core.logger import GuptLogger
from chitragupt.core.logger import LogType


class ImageUtils:
    def __init__(
        self,
        logger: GuptLogger,
    ) -> None:
        self.logger = logger

    def validate_image(
        self,
        path: pathlib.Path,
    ) -> bool:
        if not path.exists() or not path.is_file():
            self.logger.log(
                f"Invalid file path: {path}",
                LogType.ERROR,
            )
            return False
        (
            mime_type,
            _,
        ) = mimetypes.guess_type(path)
        if mime_type and mime_type.startswith("image"):
            return True
        self.logger.log(
            f"File is not an image: {path}",
            LogType.ERROR,
        )
        return False

    def get_image_metadata(
        self,
        path: pathlib.Path,
    ) -> typing.Dict[
        str,
        typing.Any,
    ]:
        if not self.validate_image(path):
            raise ValueError("Invalid image file.")

        metadata: typing.Dict[
            str,
            typing.Any,
        ] = {}
        try:
            image = PIL.Image.open(path)

            exif_data = image._getexif()  # type: ignore
            if exif_data:
                metadata = {
                    PIL.ExifTags.TAGS.get(
                        tag,
                        tag,
                    ): value  # type: ignore
                    for tag, value in exif_data.items()  # type: ignore
                }
        except Exception as e:
            self.logger.log(
                f"Failed to extract metadata: {e}",
                LogType.ERROR,
            )
        return metadata

    def calculate_hash(
        self,
        path: pathlib.Path,
        method: str = "sha256",
    ) -> str:
        if not self.validate_image(path):
            raise ValueError("Invalid image file.")
        hash_func = getattr(
            hashlib,
            method,
            None,
        )
        if not hash_func:
            raise ValueError("Unsupported hash method.")

        hasher = hash_func()
        with path.open("rb") as f:
            for chunk in iter(
                lambda: f.read(4096),
                b"",
            ):
                hasher.update(chunk)
        return hasher.hexdigest()

    def encode_base64self(
        self,
        path: pathlib.Path,
    ) -> str:
        if not self.validate_image(path):
            raise ValueError("Invalid image file.")
        with path.open("rb") as f:
            return base64.b64encode(f.read()).decode("utf-8")

    def reverse_image_search(
        self,
        path: pathlib.Path,
    ) -> typing.List[str]:
        if not self.validate_image(path):
            raise ValueError("Invalid image file.")

        return [
            f"https://www.google.com/searchbyimage?image_url={path}",
            f"https://www.bing.com/images/search?q=imgurl:{path}",
            f"https://yandex.com/images/search?rpt=imageview&url={path}",
        ]

    def detect_faces(
        self,
        path: pathlib.Path,
    ) -> typing.List[
        typing.Tuple[
            int,
            int,
            int,
            int,
        ]
    ]:
        if not self.validate_image(path):
            raise ValueError("Invalid image file.")

        face_cascade = cv2.CascadeClassifier(
            cv2.data.haarcascades + "haarcascade_frontalface_default.xml"
        )
        image = cv2.imread(str(path))
        gray = cv2.cvtColor(
            image,
            cv2.COLOR_BGR2GRAY,
        )
        faces = face_cascade.detectMultiScale(
            gray,
            scaleFactor=1.1,
            minNeighbors=5,
            minSize=(
                30,
                30,
            ),
        )
        return [tuple(face) for face in faces]  # type: ignore

    def image_difference(
        self,
        path1: pathlib.Path,
        path2: pathlib.Path,
    ) -> float:
        if not self.validate_image(path1) or not self.validate_image(path2):
            raise ValueError("Invalid image files.")

        image1 = cv2.imread(str(path1))
        image2 = cv2.imread(str(path2))

        if image1.shape != image2.shape:
            raise ValueError("Images must have the same dimensions.")

        diff = numpy.sum(
            (image1.astype("float") - image2.astype("float")) ** 2
        )
        diff /= float(image1.shape[0] * image1.shape[1])
        return diff  # type: ignore

    def extract_text_from_image(
        self,
        path: pathlib.Path,
    ) -> typing.Optional[str]:
        try:
            if not self.validate_image(path):
                raise ValueError("Invalid image file.")
            image = cv2.imread(str(path))
            text = pytesseract.pytesseract.image_to_string(image)  # type: ignore
            assert isinstance(
                text,
                str,
            )
            return text.strip()
        except Exception as e:
            self.logger.log(
                f"Error extracting text: {e}",
                LogType.ERROR,
            )
            return None

    def is_blurry(
        self,
        path: pathlib.Path,
        threshold: float = 100.0,
    ) -> bool:
        """Check if an image is blurry using the Laplacian variance method."""
        if not self.validate_image(path):
            raise ValueError("Invalid image file.")
        image = cv2.imread(
            str(path),
            cv2.IMREAD_GRAYSCALE,
        )
        variance = cv2.Laplacian(
            image,
            cv2.CV_64F,
        ).var()
        return variance < threshold

    def resize_image(
        self,
        path: pathlib.Path,
        width: typing.Optional[int] = None,
        height: typing.Optional[int] = None,
    ) -> numpy.ndarray:
        """Resize image while maintaining aspect ratio."""
        if not self.validate_image(path):
            raise ValueError("Invalid image file.")

        image = cv2.imread(str(path))
        (
            h,
            w,
        ) = image.shape[:2]

        if width and not height:
            # Calculate height based on width to maintain aspect ratio
            height = int((width / w) * h)
        elif height and not width:
            # Calculate width based on height to maintain aspect ratio
            width = int((height / h) * w)
        elif not width:
            raise ValueError("Either width or height must be specified.")

        # Resize and return the image
        return cv2.resize(
            image,
            (
                width,
                height,
            ),  # type: ignore
            interpolation=cv2.INTER_AREA,
        )

    def convert_to_grayscale(
        self,
        path: pathlib.Path,
    ) -> numpy.ndarray:
        """Convert an image to grayscale."""
        if not self.validate_image(path):
            raise ValueError("Invalid image file.")
        image = cv2.imread(str(path))
        return cv2.cvtColor(
            image,
            cv2.COLOR_BGR2GRAY,
        )

    def compare_histograms(
        self,
        path1: pathlib.Path,
        path2: pathlib.Path,
    ) -> float:
        """Compare image histograms to determine similarity (0 = different, 1 = identical)."""
        if not self.validate_image(path1) or not self.validate_image(path2):
            raise ValueError("Invalid image files.")

        image1 = cv2.imread(str(path1))
        image2 = cv2.imread(str(path2))

        image1 = cv2.cvtColor(
            image1,
            cv2.COLOR_BGR2GRAY,
        )
        image2 = cv2.cvtColor(
            image2,
            cv2.COLOR_BGR2GRAY,
        )

        hist1 = cv2.calcHist(
            [image1],
            [0],
            None,
            [256],
            [
                0,
                256,
            ],
        )
        hist2 = cv2.calcHist(
            [image2],
            [0],
            None,
            [256],
            [
                0,
                256,
            ],
        )

        hist1 = cv2.normalize(
            hist1,
            hist1,
        ).flatten()
        hist2 = cv2.normalize(
            hist2,
            hist2,
        ).flatten()

        similarity = cv2.compareHist(
            hist1,
            hist2,
            cv2.HISTCMP_CORREL,
        )
        return float(similarity)

    def find_edges(
        self,
        path: pathlib.Path,
    ) -> numpy.ndarray:
        """Detect edges in an image using the Canny algorithm."""
        if not self.validate_image(path):
            raise ValueError("Invalid image file.")
        image = cv2.imread(
            str(path),
            cv2.IMREAD_GRAYSCALE,
        )
        return cv2.Canny(
            image,
            100,
            200,
        )

    def convert_image_format(
        self,
        path: pathlib.Path,
        output_format: str,
    ) -> pathlib.Path:
        """Convert an image to a different format (e.g., PNG to JPG)."""
        if not self.validate_image(path):
            raise ValueError("Invalid image file.")

        output_path = path.with_suffix(f".{output_format.lower()}")
        image = PIL.Image.open(path)
        image.save(output_path)
        return output_path

    def detect_tampering(
        self,
        path: pathlib.Path,
    ) -> numpy.ndarray:
        """Perform Error Level Analysis (ELA) to detect image tampering."""
        if not self.validate_image(path):
            raise ValueError("Invalid image file.")

        image = PIL.Image.open(path).convert("RGB")
        temp_path = path.with_suffix(".temp.jpg")

        image.save(
            temp_path,
            quality=90,
        )
        compressed = PIL.Image.open(temp_path)

        ela_image = numpy.abs(
            numpy.array(
                image,
                dtype=numpy.int16,
            )
            - numpy.array(
                compressed,
                dtype=numpy.int16,
            )
        )
        return numpy.array(
            ela_image,
            dtype=numpy.uint8,
        )

    def detect_steganography(
        self,
        path: pathlib.Path,
    ) -> bool:
        """Check if an image contains hidden steganographic data."""
        if not self.validate_image(path):
            raise ValueError("Invalid image file.")

        with path.open("rb") as f:
            data = f.read()
        # Steganography often uses trailing data or LSB manipulation
        return b"steghide" in data or data[-100:].count(b"\x00") < 50

    def extract_hidden_data(
        self,
        path: pathlib.Path,
    ) -> typing.Optional[bytes]:
        """Extract hidden data from an image using LSB steganography analysis."""
        if not self.detect_steganography(path):
            return None

        image = cv2.imread(str(path))
        binary_data = ""

        for row in image:
            for pixel in row:  # type: ignore
                for color in pixel:
                    binary_data += str(color & 1)

        hidden_bytes = [
            binary_data[i : i + 8]
            for i in range(
                0,
                len(binary_data),
                8,
            )
        ]
        extracted_data = bytes(
            int(
                byte,
                2,
            )
            for byte in hidden_bytes
            if byte.count("0") < 7
        )

        return extracted_data or None

    def detect_image_cloning(
        self,
        path: pathlib.Path,
    ) -> bool:
        """Detect cloned or duplicated regions in an image using ORB feature matching."""
        if not self.validate_image(path):
            raise ValueError("Invalid image file.")

        image = cv2.imread(str(path))
        gray = cv2.cvtColor(
            image,
            cv2.COLOR_BGR2GRAY,
        )

        orb = cv2.ORB()  # Corrected from .create()
        (
            keypoints,
            descriptors,
        ) = orb.create().detectAndCompute(
            gray,
            None,  # type: ignore
        )  # None is the mask #type: ignore

        if descriptors is None or len(keypoints) == 0:
            return False

        bf = cv2.BFMatcher(
            cv2.NORM_HAMMING,
            crossCheck=True,
        )
        matches = bf.match(
            descriptors,
            descriptors,
        )

        # Arbitrary threshold for similarity, can be adjusted based on the image
        return len(matches) > 200

    def get_camera_source(
        self,
        path: pathlib.Path,
    ) -> typing.Optional[str]:
        """Extract camera model and manufacturer from image metadata."""
        metadata = self.get_image_metadata(path)

        if "Make" in metadata and "Model" in metadata:
            return f"{metadata['Make']} {metadata['Model']}"

        return None

    def extract_geolocation(
        self,
        path: pathlib.Path,
    ) -> typing.Optional[
        typing.Tuple[
            float,
            float,
        ]
    ]:
        """Extract GPS coordinates from image metadata if available."""
        metadata = self.get_image_metadata(path)

        if "GPSInfo" not in metadata:
            return None

        gps_info = metadata["GPSInfo"]
        (
            lat,
            lon,
        ) = (
            None,
            None,
        )

        if 1 in gps_info and 2 in gps_info:
            lat = self._convert_to_degrees(gps_info[2])
            if gps_info[1] == "S":
                lat = -lat

        if 3 in gps_info and 4 in gps_info:
            lon = self._convert_to_degrees(gps_info[4])
            if gps_info[3] == "W":
                lon = -lon

        return (
            (
                lat,
                lon,
            )
            if lat and lon
            else None
        )

    def generate_color_signature(
        self,
        path: pathlib.Path,
    ) -> str:
        """Generate a unique color signature for an image."""
        if not self.validate_image(path):
            raise ValueError("Invalid image file.")

        image = cv2.imread(str(path))
        average_color = numpy.mean(
            image,
            axis=(
                0,
                1,
            ),
        )  # type: ignore
        return f"{int(average_color[0])},{int(average_color[1])},{int(average_color[2])}"

    def perceptual_hash(
        self,
        path: pathlib.Path,
    ) -> str:
        """Generate a perceptual hash (pHash) for detecting visually similar images."""
        if not self.validate_image(path):
            raise ValueError("Invalid image file.")

        image = cv2.imread(
            str(path),
            cv2.IMREAD_GRAYSCALE,
        )
        resized = cv2.resize(
            image,
            (
                32,
                32,
            ),
            interpolation=cv2.INTER_AREA,
        )
        dct = cv2.dct(numpy.float32(resized))  # type: ignore
        dct_low_freq = dct[
            :8,
            :8,
        ]

        avg_value = numpy.mean(dct_low_freq)
        binary_hash = "".join(
            "1" if val > avg_value else "0"
            for row in dct_low_freq
            for val in row
        )

        return hex(
            int(
                binary_hash,
                2,
            )
        )[2:]  # Convert binary to hex

    def detect_qr_codes(
        self,
        path: pathlib.Path,
    ) -> typing.List[str]:
        """Detect and extract QR codes from an image."""
        if not self.validate_image(path):
            raise ValueError("Invalid image file.")

        image = cv2.imread(str(path))
        detector = cv2.QRCodeDetector()
        (
            data,
            _,
            _,
        ) = detector.detectAndDecode(image)

        return [data] if data else []

    def detect_watermark(
        self,
        path: pathlib.Path,
    ) -> bool:
        """Detect if an image contains a watermark."""
        if not self.validate_image(path):
            raise ValueError("Invalid image file.")

        image = cv2.imread(
            str(path),
            cv2.IMREAD_GRAYSCALE,
        )
        edges = cv2.Canny(
            image,
            50,
            150,
        )

        white_pixel_ratio = numpy.sum(edges > 200) / edges.size
        return white_pixel_ratio > 0.02  # type: ignore # Arbitrary threshold for watermark presence

    @staticmethod
    def _convert_to_degrees(
        value: typing.Tuple[
            int,
            int,
            int,
        ],
    ) -> float:
        """Convert GPS coordinates from DMS to decimal degrees."""
        degrees = float(value[0])
        minutes = float(value[1]) / 60.0
        seconds = float(value[2]) / 3600.0
        return degrees + minutes + seconds

    def error_level_analysis(
        self,
        path: pathlib.Path,
    ) -> typing.Optional[pathlib.Path]:
        """Perform Error Level Analysis (ELA) to detect tampered regions in an image."""
        if not self.validate_image(path):
            raise ValueError("Invalid image file.")

        image = PIL.Image.open(path).convert("RGB")
        temp_path = path.with_suffix(".ela.png")

        # Save the image with slight compression
        compressed_path = path.with_suffix(".compressed.jpg")
        image.save(
            compressed_path,
            "JPEG",
            quality=95,
        )

        # Compute pixel differences
        compressed = PIL.Image.open(compressed_path)
        ela_image = PIL.ImageChops.difference(
            image,
            compressed,
        )

        # Enhance the differences
        extrema = ela_image.getextrema()
        max_diff = max(ex[1] for ex in extrema)  # type: ignore
        scale = 255.0 / max_diff if max_diff else 1  # type: ignore
        ela_image = ela_image.point(lambda p: p * scale)

        ela_image.save(temp_path)
        return temp_path

    def detect_copy_move_forgery(
        self,
        path: pathlib.Path,
    ) -> bool:
        """Detect if parts of an image were copied and moved using keypoint analysis."""
        if not self.validate_image(path):
            raise ValueError("Invalid image file.")

        image = cv2.imread(
            str(path),
            cv2.IMREAD_GRAYSCALE,
        )
        orb = cv2.ORB()
        (
            keypoints,
            descriptors,
        ) = orb.create().detectAndCompute(
            image,
            None,  # type: ignore
        )

        if descriptors is None:
            return False

        bf = cv2.BFMatcher(
            cv2.NORM_HAMMING,
            crossCheck=True,
        )
        matches = bf.match(
            descriptors,
            descriptors,
        )

        return len(matches) > 300  # Threshold to detect tampered areas

    def detect_jpeg_ghosts(
        self,
        path: pathlib.Path,
    ) -> bool:
        """Identify if an image has undergone multiple compressions, which may indicate tampering."""
        if not self.validate_image(path):
            raise ValueError("Invalid image file.")

        image = PIL.Image.open(path)
        exif_data = image.info.get("exif")

        # Check if EXIF metadata exists but the quality is suspiciously low
        return (
            bool(exif_data)
            and image.format == "JPEG"
            and image.info.get(
                "quality",
                100,
            )
            < 80
        )

    def extract_social_media_fingerprint(
        self,
        path: pathlib.Path,
    ) -> typing.Optional[str]:
        """Determine which social media platform an image originated from based on metadata."""
        if not self.validate_image(path):
            raise ValueError("Invalid image file.")

        metadata = self.get_image_metadata(path)
        software_tag = metadata.get(
            "Software",
            "",
        ).lower()

        if "facebook" in software_tag:
            return "Facebook"
        elif "twitter" in software_tag:
            return "Twitter"
        elif "instagram" in software_tag:
            return "Instagram"
        elif "whatsapp" in software_tag:
            return "WhatsApp"
        elif "snapchat" in software_tag:
            return "Snapchat"

        return None

    @staticmethod
    def find_image_in_web_archives(
        url: str,
    ) -> typing.List[str]:
        """Search for previous versions of an image in web archives (Wayback Machine, TinEye)."""
        wayback_url = (
            f"https://web.archive.org/cdx/search/cdx?url={url}&output=json"
        )
        tineye_url = f"https://tineye.com/search/?url={url}"

        return [
            wayback_url,
            tineye_url,
        ]

    def reverse_search_perceptual_hash(
        self,
        path: pathlib.Path,
    ) -> typing.List[str]:
        """Use perceptual hashing to reverse search an image."""
        if not self.validate_image(path):
            raise ValueError("Invalid image file.")

        image = PIL.Image.open(path)
        phash = imagehash.phash(image)

        return [
            f"https://www.google.com/searchbyimage?image_url={path}&phash={phash}",
            f"https://www.bing.com/images/search?q=imgurl:{path}&phash={phash}",
            f"https://yandex.com/images/search?rpt=imageview&url={path}&phash={phash}",
        ]
