"""Base Shipper class."""

from __future__ import annotations

import datetime
import email
import logging
import re
from pathlib import Path
from shutil import copyfile
from typing import Any

import anyio
import dateparser
import homeassistant.helpers.config_validation as cv
from aioimaplib import IMAP4_SSL

from custom_components.mail_and_packages import const
from custom_components.mail_and_packages.const import (
    AMAZON_DELIVERED,
    AMAZON_DELIVERED_SUBJECT,
    AMAZON_EXCEPTION,
    AMAZON_EXCEPTION_BODY,
    AMAZON_EXCEPTION_ORDER,
    AMAZON_EXCEPTION_SUBJECT,
    AMAZON_HUB,
    AMAZON_HUB_BODY,
    AMAZON_HUB_CODE,
    AMAZON_HUB_SUBJECT,
    AMAZON_HUB_SUBJECT_SEARCH,
    AMAZON_ORDER,
    AMAZON_ORDERED_SUBJECT,
    AMAZON_OTP,
    AMAZON_OTP_CODE,
    AMAZON_OTP_REGEX,
    AMAZON_OTP_SUBJECT,
    AMAZON_PACKAGES,
    AMAZON_SHIPMENT_SUBJECT,
    ATTR_COUNT,
    CONF_AMAZON_DAYS,
    CONF_AMAZON_DOMAIN,
    CONF_AMAZON_FWDS,
    CONF_DURATION,
    CONF_FORWARDING_HEADER,
    DEFAULT_AMAZON_DAYS,
)
from custom_components.mail_and_packages.utils.amazon import (
    _extract_hub_code,
    amazon_email_addresses,
    download_amazon_img,
    extract_order_numbers,
    filter_amazon_strings,
    get_decoded_subject,
    get_email_body,
    parse_amazon_arrival_date,
    search_amazon_emails,
)
from custom_components.mail_and_packages.utils.cache import EmailCache
from custom_components.mail_and_packages.utils.date import get_today
from custom_components.mail_and_packages.utils.image import (
    cleanup_images,
    generate_delivery_gif,
    random_filename,
    resize_images,
)
from custom_components.mail_and_packages.utils.imap import (
    email_fetch,
    email_search,
)

from .base import Shipper

_LOGGER = logging.getLogger(__name__)


class AmazonShipper(Shipper):
    """Amazon shipper implementation."""

    @property
    def name(self) -> str:
        """Return shipper name."""
        return "amazon"

    @classmethod
    def handles_sensor(cls, sensor_type: str) -> bool:
        """Return True if this shipper handles the given sensor type."""
        return sensor_type.startswith("amazon_") or sensor_type == AMAZON_PACKAGES

    async def process(
        self,
        account: IMAP4_SSL,
        date: str,
        sensor_type: str,
        cache: EmailCache | None = None,
    ) -> dict[str, Any]:
        """Process Amazon-specific emails."""
        forwarding_header = self.config.get(CONF_FORWARDING_HEADER, "")
        if forwarding_header and forwarding_header != "(none)":
            # Header mode: use native Amazon addresses; fwds not needed
            fwds = None
        else:
            forwarding_header = ""
            fwds = cv.ensure_list_csv(self.config.get(CONF_AMAZON_FWDS))
        days = self.config.get(CONF_AMAZON_DAYS, DEFAULT_AMAZON_DAYS)
        domain = self.config.get(CONF_AMAZON_DOMAIN)

        if sensor_type == AMAZON_PACKAGES:
            count = await self._parse_amazon_emails(
                account, "count", fwds, days, domain, cache, forwarding_header
            )
            orders = await self._parse_amazon_emails(
                account, "order", fwds, days, domain, cache, forwarding_header
            )
            return {
                AMAZON_PACKAGES: count,
                AMAZON_ORDER: orders,
            }

        if sensor_type == AMAZON_ORDER:
            result = await self._parse_amazon_emails(
                account, "order", fwds, days, domain, cache, forwarding_header
            )
            return {AMAZON_ORDER: result}

        if sensor_type == AMAZON_HUB:
            return await self._amazon_hub(account, fwds, cache, forwarding_header)

        if sensor_type == AMAZON_OTP:
            result = await self._amazon_otp(account, fwds, cache, forwarding_header)
            return {sensor_type: result}

        if sensor_type == AMAZON_EXCEPTION:
            return await self._amazon_exception(
                account, fwds, domain, cache, forwarding_header
            )

        if sensor_type == AMAZON_DELIVERED:
            image_path = self.config.get("image_path")
            image_name = self.config.get("amazon_image")
            result = await self._amazon_search(
                account,
                image_path,
                image_name,
                domain,
                fwds,
                cache,
                forwarding_header,
            )
            return {
                sensor_type: result,
                const.ATTR_AMAZON_IMAGE: image_name,
                const.ATTR_IMAGE_PATH: image_path,
            }

        return {ATTR_COUNT: 0}

    async def process_batch(
        self,
        account: IMAP4_SSL,
        date: str,
        sensors: list[str],
        cache: EmailCache,
        since_date: str | None = None,
    ) -> dict[str, Any]:
        """Process multiple Amazon sensors in batch."""
        res = {}
        for sensor in sensors:
            sensor_res = await self.process(account, date, sensor, cache)
            res.update(sensor_res)
            # Replicate coordinator dictionary logic
            if sensor not in sensor_res:
                if ATTR_COUNT in sensor_res:
                    res[sensor] = sensor_res[ATTR_COUNT]
        return res

    # Internal helper methods (migrated from helpers.py)

    async def _parse_amazon_emails(
        self,
        account: IMAP4_SSL,
        param: str,
        fwds: list[str] | None = None,
        days: int = DEFAULT_AMAZON_DAYS,
        domain: str | None = None,
        cache: EmailCache | None = None,
        forwarding_header: str = "",
    ) -> list[str] | int:
        """Parse Amazon emails for delivery date and order number."""
        today_date = get_today()
        address_list = amazon_email_addresses(fwds, domain)
        unique_emails = await search_amazon_emails(
            account, address_list, days, domain, cache, forwarding_header
        )
        order_pattern = re.compile(r"[0-9]{3}-[0-9]{7}-[0-9]{7}")

        context = {
            "today": today_date,
            "packages_arriving_today": {},
            "delivered_packages": {},
            "amazon_delivered": [],
            "deliveries_today": [],
            "all_shipped_orders": set(),
            "order_pattern": order_pattern,
        }

        for email_id in unique_emails:
            await self._process_amazon_email(account, email_id, context, cache)

        final_count = self._calculate_final_count(context)

        if param == "count":
            return final_count

        return [
            order_id
            for order_id in context["all_shipped_orders"]
            if context["packages_arriving_today"].get(order_id, 0)
            > context["delivered_packages"].get(order_id, 0)
            or (
                context["packages_arriving_today"].get(order_id, 0) == 0
                and context["delivered_packages"].get(order_id, 0) == 0
            )
        ]

    async def _process_amazon_email(
        self,
        account: IMAP4_SSL,
        email_id: bytes | str,
        ctx: dict,
        cache: EmailCache | None = None,
    ):
        """Process a single Amazon email."""
        fetch_id = email_id.decode() if isinstance(email_id, bytes) else email_id
        if cache:
            data = (await cache.fetch(fetch_id, "(RFC822)"))[1]
        else:
            data = (await email_fetch(account, fetch_id, "(RFC822)"))[1]

        for response_part in data:
            if not isinstance(response_part, (bytes, bytearray)):
                continue

            msg = email.message_from_bytes(response_part)
            email_date = await self._parse_email_date(msg)
            email_subject = get_decoded_subject(msg)

            if any(s.lower() in email_subject.lower() for s in AMAZON_ORDERED_SUBJECT):
                continue

            email_msg = get_email_body(msg)
            if any(
                s.lower() in email_subject.lower() for s in AMAZON_DELIVERED_SUBJECT
            ):
                self._handle_delivered_email(email_subject, email_msg, ctx)
                continue

            await self._handle_shipping_email(email_subject, email_msg, email_date, ctx)

    async def _parse_email_date(
        self,
        msg: email.message.Message,
    ) -> datetime.date | None:
        """Parse the date from an email message."""
        date_str = msg.get("Date")
        if not date_str:
            return None
        parsed = await self.hass.async_add_executor_job(dateparser.parse, date_str)
        return parsed.date() if parsed else None

    def _handle_delivered_email(self, subject: str, body: str | None, ctx: dict):
        """Handle an Amazon 'delivered' email."""
        orders = extract_order_numbers(subject, ctx["order_pattern"])
        if not orders and body:
            orders = extract_order_numbers(body, ctx["order_pattern"])
        for o in orders:
            ctx["delivered_packages"][o] = ctx["delivered_packages"].get(o, 0) + 1
            if o not in ctx["amazon_delivered"]:
                ctx["amazon_delivered"].append(o)

    async def _handle_shipping_email(
        self,
        subject: str,
        body: str | None,
        date: datetime.date | None,
        ctx: dict,
    ):
        """Handle an Amazon 'shipping' or 'arriving' email."""
        order_id = self._extract_first_order_id(subject, body, ctx["order_pattern"])
        if order_id:
            ctx["all_shipped_orders"].add(order_id)

        if body:
            parsed_arrival = await parse_amazon_arrival_date(self.hass, body, date)
            if parsed_arrival == ctx["today"]:
                if order_id:
                    ctx["packages_arriving_today"][order_id] = (
                        ctx["packages_arriving_today"].get(order_id, 0) + 1
                    )
                else:
                    ctx["deliveries_today"].append("Amazon Order")

    def _extract_first_order_id(
        self,
        subject: str,
        body: str | None,
        pattern: re.Pattern,
    ) -> str | None:
        """Extract the first order number found in subject or body."""
        orders = extract_order_numbers(subject, pattern)
        if orders:
            return orders[0]
        if body:
            orders = extract_order_numbers(body, pattern)
            if orders:
                return orders[0]
        return None

    def _calculate_final_count(self, ctx: dict) -> int:
        """Calculate the final count of packages arriving today."""
        deliveries_today = [
            item
            for item in ctx["deliveries_today"]
            if item not in ctx["amazon_delivered"]
        ]
        final_count = 0
        for order_id, arriving_count in ctx["packages_arriving_today"].items():
            delivered_count = ctx["delivered_packages"].get(order_id, 0)
            final_count += max(0, arriving_count - delivered_count)
        return final_count + len(deliveries_today)

    async def _amazon_search(
        self,
        account: IMAP4_SSL,
        image_path: str,
        amazon_image_name: str,
        amazon_domain: str,
        fwds: list[str] | None = None,
        cache: EmailCache | None = None,
        forwarding_header: str = "",
    ) -> int:
        """Find Amazon Delivered email and handle images."""
        _LOGGER.debug("=== AMAZON DELIVERED SEARCH START ===")
        subjects = AMAZON_DELIVERED_SUBJECT
        today = get_today().strftime("%d-%b-%Y")
        count = 0
        all_image_urls = []

        await self.hass.async_add_executor_job(
            cleanup_images,
            f"{image_path or ''}amazon/",
        )

        address_list = amazon_email_addresses(fwds, amazon_domain)
        _LOGGER.debug("Amazon email search addresses: %s", address_list)
        if amazon_domain:
            subjects = filter_amazon_strings(subjects, amazon_domain)

        (server_response, data) = await email_search(
            account=account,
            address=address_list,
            date=today,
            subject=subjects,
            header=forwarding_header,
        )
        if server_response == "OK" and data[0]:
            for email_id in data[0].split():
                fetch_id = (
                    email_id.decode() if isinstance(email_id, bytes) else email_id
                )
                if cache:
                    msg_data = (await cache.fetch(fetch_id, "(RFC822)"))[1]
                else:
                    msg_data = (await email_fetch(account, fetch_id, "(RFC822)"))[1]

                is_delivered, urls = self._is_amazon_delivered(msg_data, subjects)
                if is_delivered:
                    count += 1
                    for url in urls:
                        if url not in all_image_urls:
                            all_image_urls.append(url)

        await self._process_amazon_images(
            all_image_urls, image_path, amazon_image_name, count
        )

        return count

    def _is_amazon_delivered(
        self, msg_data: list, subjects: list[str]
    ) -> tuple[bool, list[str]]:
        """Verify if email is a delivered notification and return image URLs."""
        for response_part in msg_data:
            if not isinstance(response_part, (bytes, bytearray)):
                continue
            msg = email.message_from_bytes(response_part)
            subject = get_decoded_subject(msg)
            if not subject:
                continue

            # Check if subject contains any delivered keyword (case-insensitive)
            has_delivered = any(s.lower() in subject.lower() for s in subjects)
            # Check if subject contains ordered or shipped keywords (case-insensitive)
            has_ordered = any(
                s.lower() in subject.lower() for s in AMAZON_ORDERED_SUBJECT
            )
            has_shipped = any(
                s.lower() in subject.lower() for s in AMAZON_SHIPMENT_SUBJECT
            )

            if has_delivered and not has_ordered and not has_shipped:
                urls = self._extract_amazon_image_urls(msg)
                return True, urls
        return False, []

    def _extract_amazon_image_urls(self, msg: email.message.Message) -> list[str]:
        """Extract image URLs from Amazon email body."""
        urls = []
        pattern = re.compile(rf"{const.AMAZON_IMG_PATTERN}")
        for part in msg.walk():
            if part.get_content_type() != "text/html":
                continue
            part_payload = part.get_payload(decode=True)
            if part_payload:
                part_content = part_payload.decode("utf-8", "ignore")
                found = pattern.findall(part_content)
                for url in found:
                    if url[1] not in const.AMAZON_IMG_LIST:
                        continue
                    full_url = url[0] + url[1] + url[2]
                    if full_url not in urls:
                        urls.append(full_url)
        return urls

    async def _process_amazon_images(
        self,
        image_urls: list[str],
        image_base_path: str,
        image_name: str,
        email_count: int,
    ) -> None:
        """Process and save Amazon delivery images."""
        if not image_base_path or not image_name:
            return

        amazon_path = Path(image_base_path) / "amazon"
        image_files = await self._download_all_images(image_urls, image_base_path)

        if len(image_files) > 1:
            await self._create_amazon_gif(image_files, amazon_path, image_name)
        elif len(image_files) == 1:
            await self._save_single_amazon_image(
                image_files[0], amazon_path, image_name
            )
        else:
            await self._copy_amazon_placeholder(amazon_path, image_name)

    async def _download_all_images(self, urls: list[str], base_path: str) -> list[str]:
        """Download all image URLs to temporary files."""
        image_files = []
        amazon_path = Path(base_path) / "amazon"
        for url in urls:
            temp_filename = random_filename()
            await download_amazon_img(url, base_path, temp_filename, self.hass)
            full_temp_path = amazon_path / temp_filename
            if await anyio.Path(full_temp_path).exists():
                image_files.append(str(full_temp_path))
        return image_files

    async def _create_amazon_gif(
        self, image_files: list[str], amazon_path: Path, image_name: str
    ) -> None:
        """Create animated GIF from multiple images."""
        _LOGGER.debug("Combining %d Amazon images into GIF", len(image_files))
        resized_images = await self.hass.async_add_executor_job(
            resize_images, image_files, 724, 320
        )
        gif_path = str(amazon_path / image_name)
        duration = self.config.get(CONF_DURATION, 5) * 1000
        await self.hass.async_add_executor_job(
            generate_delivery_gif, resized_images, gif_path, duration
        )
        # Cleanup
        for img in image_files + resized_images:
            if await anyio.Path(img).exists():
                await self.hass.async_add_executor_job(
                    cleanup_images, str(Path(img).parent) + "/", Path(img).name
                )

    async def _save_single_amazon_image(
        self, image_file: str, amazon_path: Path, image_name: str
    ) -> None:
        """Save a single image by renaming it to the final name."""
        final_path = amazon_path / image_name
        if await anyio.Path(final_path).exists():
            await anyio.Path(final_path).unlink()
        await self.hass.async_add_executor_job(Path(image_file).rename, final_path)
        _LOGGER.debug("Single Amazon image saved: %s", image_name)

    async def _copy_amazon_placeholder(
        self, amazon_path: Path, image_name: str
    ) -> None:
        """Copy the Amazon no-delivery placeholder."""
        nomail = f"{Path(__file__).parent.parent}/no_deliveries_amazon.jpg"
        _LOGGER.debug("No Amazon images found in emails, using placeholder")
        try:
            await self.hass.async_add_executor_job(
                copyfile, nomail, str(amazon_path / image_name)
            )
        except OSError as err:
            _LOGGER.error("Error attempting to copy image: %s", err)

    async def _amazon_hub(
        self,
        account: IMAP4_SSL,
        fwds: list[str] | None = None,
        cache: EmailCache | None = None,
        forwarding_header: str = "",
    ) -> dict[str, Any]:
        """Find Amazon Hub code."""
        _LOGGER.debug("=== AMAZON HUB SEARCH START ===")
        count = 0
        code = []
        processed_ids = []
        today = get_today().strftime("%d-%b-%Y")
        address_list = amazon_email_addresses(fwds, "amazon.com")
        for search_subject in AMAZON_HUB_SUBJECT:
            (server_response, data) = await email_search(
                account,
                address_list,
                today,
                search_subject,
                body=AMAZON_HUB_BODY,
                header=forwarding_header,
            )
            if server_response == "OK" and data[0] is not None:
                for num in data[0].split():
                    if num in processed_ids:
                        continue
                    processed_ids.append(num)
                    if cache:
                        msg_parts = (await cache.fetch(num, "(RFC822)"))[1]
                    else:
                        msg_parts = (await email_fetch(account, num, "(RFC822)"))[1]
                    for response_part in msg_parts:
                        if isinstance(response_part, (bytes, bytearray)):
                            msg = email.message_from_bytes(response_part)
                            actual_subject = get_decoded_subject(msg)
                            body = get_email_body(msg)
                            if hub_code := _extract_hub_code(
                                body,
                                AMAZON_HUB_BODY,
                                actual_subject,
                                AMAZON_HUB_SUBJECT_SEARCH,
                            ):
                                count += 1
                                if hub_code not in code:
                                    code.append(hub_code)
        return {AMAZON_HUB: count, AMAZON_HUB_CODE: code}

    async def _amazon_otp(
        self,
        account: IMAP4_SSL,
        fwds: list[str] | None = None,
        cache: EmailCache | None = None,
        forwarding_header: str = "",
    ) -> dict[str, Any]:
        """Find Amazon OTP code."""
        code = []
        today = get_today().strftime("%d-%b-%Y")
        address_list = amazon_email_addresses(fwds, "amazon.com")
        (server_response, data) = await email_search(
            account,
            address_list,
            today,
            AMAZON_OTP_SUBJECT,
            body=AMAZON_OTP_REGEX,
            header=forwarding_header,
        )
        if server_response == "OK" and data[0] is not None:
            for num in data[0].split():
                if cache:
                    msg_parts = (await cache.fetch(num, "(RFC822)"))[1]
                else:
                    msg_parts = (await email_fetch(account, num, "(RFC822)"))[1]
                for response_part in msg_parts:
                    if isinstance(response_part, (bytes, bytearray)):
                        msg = email.message_from_bytes(response_part)
                        body = get_email_body(msg)
                        if (
                            found := re.compile(AMAZON_OTP_REGEX).search(body)
                        ) is not None:
                            code.append(found.group(2))
        return {AMAZON_OTP: len(code), AMAZON_OTP_CODE: code}

    async def _amazon_exception(
        self,
        account: IMAP4_SSL,
        fwds: list[str] | None = None,
        domain: str | None = None,
        cache: EmailCache | None = None,
        forwarding_header: str = "",
    ) -> dict[str, Any]:
        """Find Amazon exception emails."""
        count = 0
        orders = []
        today = get_today().strftime("%d-%b-%Y")
        address_list = amazon_email_addresses(fwds, domain)
        (server_response, data) = await email_search(
            account=account,
            address=address_list,
            date=today,
            subject=AMAZON_EXCEPTION_SUBJECT,
            header=forwarding_header,
        )
        if server_response == "OK" and data[0] is not None:
            order_pattern = re.compile(r"[0-9]{3}-[0-9]{7}-[0-9]{7}")
            for num in data[0].split():
                if cache:
                    msg_parts = (await cache.fetch(num, "(RFC822)"))[1]
                else:
                    msg_parts = (await email_fetch(account, num, "(RFC822)"))[1]
                for response_part in msg_parts:
                    if isinstance(response_part, (bytes, bytearray)):
                        msg = email.message_from_bytes(response_part)
                        body = get_email_body(msg)
                        subject = get_decoded_subject(msg)
                        if AMAZON_EXCEPTION_BODY in body:
                            count += 1
                            if found := order_pattern.findall(body):
                                orders.extend(found)
                            if found := order_pattern.findall(subject):
                                orders.extend(found)
        return {AMAZON_EXCEPTION: count, AMAZON_EXCEPTION_ORDER: orders}
