"""Logic all around a boostable status."""

import re
from dataclasses import dataclass
from dataclasses import field
from hashlib import sha256
from urllib.parse import urlparse

from httpx import AsyncClient
from loguru import logger as log
from minimal_activitypub import Status

from fedibooster.cache_service import CacheService
from fedibooster.config import NoReblog


@dataclass
class Boostable:
    """Represents a status that can be boosted."""

    status: Status
    cache_service: CacheService
    no_reblog: NoReblog
    reblog_langs: list[str]
    attachments_hashes: list[str] = field(default_factory=list)

    @property
    def status_url(self) -> str:
        """Get the status URL."""
        return self.status.get("url", "")

    @property
    def status_id(self) -> str | None:
        """Get the status ID."""
        return self.status.get("id")

    @property
    def status_apid(self) -> str | None:
        """Get the status AP ID."""
        return self.status.get("ap_id")

    @property
    def status_language(self) -> str | None:
        """Get the status language."""
        return self.status.get("language")

    @property
    def status_sensitive(self) -> bool:
        """Check if status is marked as sensitive."""
        return self.status.get("sensitive", False)

    @property
    def status_account(self) -> dict:
        """Get the status account information."""
        return self.status.get("account", {})

    @property
    def status_media_attachments(self) -> list:
        """Get the status media attachments."""
        return self.status.get("media_attachments", [])

    @property
    def username(self) -> str:
        """Get the username from the status account."""
        return self.status_account.get("acct", "")

    @property
    def status_tags(self) -> list[dict]:
        """Get the status tags."""
        return self.status.get("tags", [])

    def bot_status(self) -> bool:
        """Check if status has been made by a bot account."""
        if self.status_account.get("bot"):
            self._log_no_boost("it was posted by a bot")
            return True

        return False

    def sensitive_status_blocked(self, reblog_sensitive: bool) -> bool:
        """Check if the status is marked as sensitive and if check if we allow rebloging sensitve statuses."""
        if self.status_sensitive and not reblog_sensitive:
            self._log_no_boost("it is a sensitive status")
            return True

        return False

    def no_attachments(self) -> bool:
        """Check if the status has NO attachments."""
        if not self.status_media_attachments:
            self._log_no_boost("it has no attachments / media")
            return True

        return False

    def too_many_tags(self, limit: int | None) -> bool:
        """Determine if status has more than `limit` tags."""
        if limit and len(self.status_tags) > limit:
            self._log_no_boost(f"it has more than {limit} tags")
            return True

        return False

    def _log_no_boost(self, reason: str) -> None:
        """Log why a status is not being boosted."""
        log.opt(colors=True).debug(
            f"<dim><red>Not Boosting</red> <cyan>{self.status_url}</cyan> because {reason}</dim>"
        )

    def by_no_reblog_user(self, search_host: str) -> bool:
        """Check if status was posted by a user in the no_reblog_users list."""
        username = self._get_expanded_username(search_host)

        # Check exact matches
        for no_reblog_user in self.no_reblog.users_list:
            if re.search(rf"{no_reblog_user}", username):
                self._log_no_boost(
                    f"it was posted by a '{username}' who is a match for {no_reblog_user} in the no_reblog_users list"
                )
                return True

        # Check regex matches
        if any(p.search(username) for p in self.no_reblog.users_regexes):
            self._log_no_boost(f"it was posted by a '{username}' who matches a regex in no_reblog_users_regex")
            return True

        return False

    def _get_expanded_username(self, search_host: str) -> str:
        """Get and expand username if needed."""
        if "@" not in self.username:
            username = self.expand_local_user(username=self.username, search_host=search_host)
        else:
            username = self.username
        log.debug(f"{username=}")
        return username

    @staticmethod
    def expand_local_user(username: str, search_host: str) -> str:
        """Expand username that doesn't contain a hostname."""
        # If username already has a domain, return it unchanged
        if "@" in username:
            return username

        parsed = urlparse(url=search_host)
        hostname = parsed.hostname or search_host
        expanded_username = f"{username}@{hostname}"
        log.debug(f"Expanded {username} to {expanded_username}")
        return expanded_username

    def has_no_reblog_tag(self) -> bool:
        """Check if status contains any tag in the no_reblog_tags list or matches any regex."""
        status_tags = self._get_status_tags()

        # Check exact matches
        if any(no_reblog.casefold() in status_tags for no_reblog in self.no_reblog.tags_list):
            self._log_no_boost("it contains tags that are in the no_reblog_tags list")
            return True

        # Check regex matches
        if any(pattern.search(tag) for tag in status_tags for pattern in self.no_reblog.tags_regexs):
            self._log_no_boost("it contains tags that match a regex in no_reblog_tags_regex")
            return True

        return False

    def _get_status_tags(self) -> list[str]:
        """Get and log status tags (casefolded for matching)."""
        status_tags: list[str] = [x["name"].casefold() for x in self.status_tags]
        log.debug(f"{status_tags=}")
        return status_tags

    def record(self) -> None:
        """Record a status having been rebloged."""
        self.cache_service.record_boost(self)

    def has_already_been_boosted(self) -> bool:
        """Perform a number of checks to see if status has already been boosted."""
        if self.cache_service.has_been_boosted(self.status_id, self.status_url, self.status_apid):
            self.record()
            return True
        return False

    async def have_attachments_already_been_boosted(self, client: AsyncClient) -> None:
        """Check if at least one attachment has already been boosted.

        If all attachments have not been boosted, return a list of attachment hashes
        otherwise return empty list
        """
        for attachment in self.status_media_attachments:
            attachment_hash = await determine_attachment_hash(url=attachment.get("url"), client=client)
            if self.cache_service.get_attachment_cache_status(attachment_hash):
                log.opt(colors=True).info(
                    f"<dim><red>Not Boosting:</red> At least one attachment of status at "
                    f"<cyan>{self.status_url}</cyan> has already been boosted or posted.</dim>"
                )
                self.attachments_hashes = []
                self.record()
                return

            self.attachments_hashes.append(attachment_hash)
            self.cache_service.record_attachment(attachment_hash)

    def should_be_boosted(
        self,
        reblog_sensitive: bool,
        search_instance: str,
        limit_tags: int | None,
    ) -> bool:
        """Determine if this should be boosted."""
        if (
            self.bot_status()
            or self.not_in_my_languages()
            or self.sensitive_status_blocked(reblog_sensitive=reblog_sensitive)
            or self.no_attachments()
            or self.has_no_reblog_tag()
            or self.by_no_reblog_user(search_host=search_instance)
            or self.too_many_tags(limit=limit_tags)
        ):
            self.record()
            return False

        return True

    def not_in_my_languages(self) -> bool:
        """Check the status is not in any of my desired languages."""
        log.debug(f"{self.status_language=}")
        if self.reblog_langs and self.status_language and self.status_language not in self.reblog_langs:
            self._log_no_boost("it is not in a desired language")
            return True

        return False


async def determine_attachment_hash(url: str, client: AsyncClient) -> str:
    """Determine attachment hash."""
    response = await client.get(url=url)
    url_hash = sha256(response.content).hexdigest()
    return url_hash
