"""Ad operations mixin.

Provides list_ads, get_ad_policy_details, create_ad, update_ad, update_ad_status.
"""

from __future__ import annotations

import logging
from typing import TYPE_CHECKING, Any

from google.protobuf.field_mask_pb2 import FieldMask as PbFieldMask

from mureo.google_ads._rsa_validator import (
    RSAValidationResult,
    predict_ad_strength,
    validate_rsa_texts,
)
from mureo.google_ads.client import _wrap_mutate_error
from mureo.google_ads.mappers import (
    map_ad_strength,
    map_ad_type,
    map_approval_status,
    map_entity_status,
    map_policy_topic_type,
    map_review_status,
)

if TYPE_CHECKING:
    from google.ads.googleads.client import GoogleAdsClient

logger = logging.getLogger(__name__)


class _AdsMixin:
    """Ad listing, details, creation, and status changes."""

    _customer_id: str
    _client: GoogleAdsClient

    @staticmethod
    def _validate_id(value: str, field_name: str) -> str: ...  # type: ignore[empty-body]
    @staticmethod
    def _validate_status(status: str) -> str: ...  # type: ignore[empty-body]
    def _get_service(self, service_name: str) -> Any: ...
    async def _search(self, query: str) -> Any: ...
    @staticmethod
    def _extract_evidences(entry: Any) -> list[str]: ...  # type: ignore[empty-body]

    async def _assert_ad_is_rsa(self, ad_id: str) -> None:
        """Raise ValueError if the target ad is not a Responsive Search Ad.

        ``update_ad`` only supports RSAs today. RDAs (display ads) and
        other ad types cannot be text-updated through this code path.
        """
        query = (
            "SELECT ad_group_ad.ad.type FROM ad_group_ad "
            f"WHERE ad_group_ad.ad.id = {ad_id}"
        )
        response = await self._search(query)
        for row in response:
            ad_type_str = map_ad_type(row.ad_group_ad.ad.type_)
            if ad_type_str != "RESPONSIVE_SEARCH_AD":
                raise ValueError(
                    f"update_ad supports RSA (Responsive Search Ad) only. "
                    f"Ad {ad_id} is type {ad_type_str}. "
                    f"For display ads, recreate via create_display_ad."
                )
            return
        raise ValueError(
            f"Ad {ad_id} not found in customer {self._customer_id}. "
            f"Verify the ad_id and that the credentials (login_customer_id) "
            f"have access to this account."
        )

    async def _get_rsa_assets(self, ad_id: str) -> tuple[list[str], list[str]]:
        """Read an RSA's current headline and description texts.

        Used to backfill the side of an ``update_ad`` the caller did not
        supply, so a single-side update never wipes the other side.
        """
        query = (
            "SELECT ad_group_ad.ad.responsive_search_ad.headlines, "
            "ad_group_ad.ad.responsive_search_ad.descriptions "
            f"FROM ad_group_ad WHERE ad_group_ad.ad.id = {ad_id}"
        )
        response = await self._search(query)
        for row in response:
            rsa = row.ad_group_ad.ad.responsive_search_ad
            headlines = [asset.text for asset in rsa.headlines]
            descriptions = [asset.text for asset in rsa.descriptions]
            return headlines, descriptions
        return [], []

    # === Ads ===

    @staticmethod
    def _validate_and_prepare_rsa(
        headlines: list[str],
        descriptions: list[str],
        final_url: str,
    ) -> tuple[list[str], list[str], RSAValidationResult]:
        """Common processing for RSA validation + limit truncation + minimum count check."""
        rsa_result = validate_rsa_texts(headlines, descriptions, final_url)
        headlines = list(rsa_result.headlines)
        descriptions = list(rsa_result.descriptions)
        if rsa_result.warnings:
            logger.warning("RSA validation warnings: %s", rsa_result.warnings)

        if len(headlines) > 15:
            logger.info(
                "Truncating headlines exceeding limit: %d -> 15", len(headlines)
            )
            headlines = headlines[:15]
        if len(descriptions) > 4:
            logger.info(
                "Truncating descriptions exceeding limit: %d -> 4", len(descriptions)
            )
            descriptions = descriptions[:4]

        if len(headlines) < 3:
            raise ValueError(
                f"At least 3 headlines are required (currently {len(headlines)})"
            )
        if len(descriptions) < 2:
            raise ValueError(
                f"At least 2 descriptions are required (currently {len(descriptions)})"
            )
        return headlines, descriptions, rsa_result

    @staticmethod
    def _build_ad_strength_result(
        result: dict[str, Any],
        rsa_result: RSAValidationResult,
        headlines: list[str],
        descriptions: list[str],
        keywords: list[str] | None,
    ) -> dict[str, Any]:
        """Common processing to add Ad Strength prediction results to result."""
        if rsa_result.warnings:
            result["warnings"] = list(rsa_result.warnings)

        ad_strength = predict_ad_strength(
            headlines=headlines,
            descriptions=descriptions,
            keywords=keywords,
        )
        result["ad_strength"] = {
            "level": ad_strength.level,
            "score": round(ad_strength.score, 2),
            "suggestions": list(ad_strength.suggestions),
        }
        if ad_strength.level == "POOR":
            result["warnings"] = result.get("warnings", []) + [
                f"Ad Strength prediction: {ad_strength.level} ({ad_strength.score:.0%}) - improvement recommended"
            ]
        return result

    async def list_ads(
        self,
        ad_group_id: str | None = None,
        status_filter: str | None = None,
    ) -> list[dict[str, Any]]:
        """List ads. Returns RSA and RDA text fields when applicable."""
        query = """
            SELECT
                ad_group_ad.ad.id, ad_group_ad.ad.name,
                ad_group_ad.ad.type, ad_group_ad.status,
                ad_group_ad.ad_strength,
                ad_group_ad.ad.final_urls,
                ad_group_ad.ad.responsive_search_ad.headlines,
                ad_group_ad.ad.responsive_search_ad.descriptions,
                ad_group_ad.ad.responsive_display_ad.headlines,
                ad_group_ad.ad.responsive_display_ad.long_headline,
                ad_group_ad.ad.responsive_display_ad.descriptions,
                ad_group_ad.ad.responsive_display_ad.business_name,
                ad_group_ad.ad.responsive_display_ad.marketing_images,
                ad_group_ad.ad.responsive_display_ad.square_marketing_images,
                ad_group_ad.ad.responsive_display_ad.logo_images,
                ad_group_ad.policy_summary.review_status,
                ad_group_ad.policy_summary.approval_status,
                ad_group.id, ad_group.name,
                campaign.id, campaign.name, campaign.status
            FROM ad_group_ad
        """
        conditions: list[str] = []
        if ad_group_id:
            self._validate_id(ad_group_id, "ad_group_id")
            conditions.append(
                f"ad_group_ad.ad_group = 'customers/{self._customer_id}/adGroups/{ad_group_id}'"
            )
        if status_filter:
            validated = self._validate_status(status_filter)
            conditions.append(f"ad_group_ad.status = '{validated}'")
        if conditions:
            query += "\n            WHERE " + " AND ".join(conditions)
        response = await self._search(query)
        results = []
        for row in response:
            ps = row.ad_group_ad.policy_summary
            ad_type = map_ad_type(row.ad_group_ad.ad.type_)
            headlines: list[str] = []
            descriptions: list[str] = []
            entry: dict[str, Any] = {
                "id": str(row.ad_group_ad.ad.id),
                "ad_group_id": str(row.ad_group.id),
                "ad_group_name": row.ad_group.name,
                "campaign_id": str(row.campaign.id),
                "campaign_name": row.campaign.name,
                "campaign_status": map_entity_status(row.campaign.status),
                "status": map_entity_status(row.ad_group_ad.status),
                "type": ad_type,
                "ad_strength": map_ad_strength(row.ad_group_ad.ad_strength),
                "final_urls": list(row.ad_group_ad.ad.final_urls),
                "review_status": map_review_status(ps.review_status) if ps else "",
                "approval_status": (
                    map_approval_status(ps.approval_status) if ps else ""
                ),
            }
            # Populate headlines/descriptions for RSA and RDA. Other ad
            # types (image, video, call, app, etc.) fall through with the
            # empty lists initialized above — no RSA/RDA-specific keys
            # leak into those entries.
            if ad_type == "RESPONSIVE_SEARCH_AD":
                rsa = row.ad_group_ad.ad.responsive_search_ad
                headlines = (
                    [asset.text for asset in rsa.headlines] if rsa.headlines else []
                )
                descriptions = (
                    [asset.text for asset in rsa.descriptions]
                    if rsa.descriptions
                    else []
                )
            elif ad_type == "RESPONSIVE_DISPLAY_AD":
                rda = row.ad_group_ad.ad.responsive_display_ad
                headlines = (
                    [asset.text for asset in rda.headlines] if rda.headlines else []
                )
                descriptions = (
                    [asset.text for asset in rda.descriptions]
                    if rda.descriptions
                    else []
                )
                # long_headline is a singular AdTextAsset on the proto,
                # so .text is always present — we use `or ""` to guard
                # against the empty string case without a None check.
                entry["long_headline"] = rda.long_headline.text or ""
                entry["business_name"] = rda.business_name or ""
                entry["marketing_images"] = (
                    [img.asset for img in rda.marketing_images]
                    if rda.marketing_images
                    else []
                )
                entry["square_marketing_images"] = (
                    [img.asset for img in rda.square_marketing_images]
                    if rda.square_marketing_images
                    else []
                )
                entry["logo_images"] = (
                    [img.asset for img in rda.logo_images] if rda.logo_images else []
                )
            entry["headlines"] = headlines
            entry["descriptions"] = descriptions
            results.append(entry)
        return results

    async def get_ad_policy_details(
        self, ad_group_id: str, ad_id: str
    ) -> dict[str, Any] | None:
        """Get ad policy details (disapproval reasons, etc.)."""
        self._validate_id(ad_group_id, "ad_group_id")
        self._validate_id(ad_id, "ad_id")
        query = f"""
            SELECT
                ad_group_ad.ad.id,
                ad_group_ad.status,
                ad_group_ad.policy_summary.approval_status,
                ad_group_ad.policy_summary.review_status,
                ad_group_ad.policy_summary.policy_topic_entries
            FROM ad_group_ad
            WHERE ad_group.id = {ad_group_id}
                AND ad_group_ad.ad.id = {ad_id}
        """
        response = await self._search(query)
        for row in response:
            ps = row.ad_group_ad.policy_summary
            policy_issues: list[dict[str, Any]] = []
            if ps and ps.policy_topic_entries:
                for entry in ps.policy_topic_entries:
                    policy_issues.append(
                        {
                            "topic": str(entry.topic),
                            "type": map_policy_topic_type(entry.type_),
                            "evidences": self._extract_evidences(entry),
                        }
                    )
            return {
                "ad_id": str(row.ad_group_ad.ad.id),
                "status": map_entity_status(row.ad_group_ad.status),
                "approval_status": (
                    map_approval_status(ps.approval_status) if ps else ""
                ),
                "review_status": map_review_status(ps.review_status) if ps else "",
                "policy_issues": policy_issues,
            }
        return None

    @_wrap_mutate_error("ad creation")
    async def create_ad(self, params: dict[str, Any]) -> dict[str, Any]:
        """Create a responsive search ad."""
        final_url = params.get("final_url", "")
        headlines, descriptions, rsa_result = self._validate_and_prepare_rsa(
            params.get("headlines", []),
            params.get("descriptions", []),
            final_url,
        )

        ad_group_ad_service = self._get_service("AdGroupAdService")
        op = self._client.get_type("AdGroupAdOperation")
        ad_group_ad = op.create
        ad_group_ad.ad_group = self._client.get_service("AdGroupService").ad_group_path(
            self._customer_id, params["ad_group_id"]
        )
        ad_group_ad.status = self._client.enums.AdGroupAdStatusEnum.PAUSED
        ad = ad_group_ad.ad
        for h in headlines:
            text_asset = self._client.get_type("AdTextAsset")
            text_asset.text = h
            ad.responsive_search_ad.headlines.append(text_asset)
        for d in descriptions:
            text_asset = self._client.get_type("AdTextAsset")
            text_asset.text = d
            ad.responsive_search_ad.descriptions.append(text_asset)
        ad.final_urls.append(final_url)
        response = ad_group_ad_service.mutate_ad_group_ads(
            customer_id=self._customer_id,
            operations=[op],
        )
        result: dict[str, Any] = {
            "resource_name": response.results[0].resource_name,
        }
        return self._build_ad_strength_result(
            result,
            rsa_result,
            headlines,
            descriptions,
            params.get("keywords"),
        )

    @_wrap_mutate_error("ad text update")
    async def update_ad(self, params: dict[str, Any]) -> dict[str, Any]:
        """Update headlines and/or descriptions of an existing RSA.

        Uses AdService.mutate_ads (not AdGroupAdService). A supplied side
        (headlines or descriptions) is **fully replaced** — Google has no
        per-asset patch. To avoid wiping the side you are not changing, omit
        it: the omitted side is read from the current ad and left untouched
        (its FieldMask path is not set). Supply at least one of headlines /
        descriptions / final_url.

        This method currently supports RSA (Responsive Search Ad) only.
        If the target ad is a RDA (Responsive Display Ad), it raises
        ValueError with a clear message. RDA updates are not yet
        implemented; recreate the ad via ``create_display_ad``.
        """
        ad_id = params.get("ad_id", "")
        self._validate_id(ad_id, "ad_id")
        final_url = params.get("final_url")

        # Pre-check the target ad type. Only RSAs are supported; bailing
        # out early avoids the API returning a cryptic "An error occurred
        # while processing ad text update." message for display ads.
        await self._assert_ad_is_rsa(ad_id)

        supplied_headlines = params.get("headlines")
        supplied_descriptions = params.get("descriptions")
        if not supplied_headlines and not supplied_descriptions and not final_url:
            raise ValueError(
                "Supply headlines and/or descriptions (and/or final_url) to update."
            )

        # Read current assets only when a side is omitted, so the omitted
        # side can be preserved (single-side update never wipes the other).
        if supplied_headlines is None or supplied_descriptions is None:
            current_headlines, current_descriptions = await self._get_rsa_assets(ad_id)
        else:
            current_headlines, current_descriptions = [], []
        # If a side was omitted but its current assets could not be read, fail
        # with a clear message rather than a misleading "min count" error from
        # validation below (which would blame the side the caller never touched).
        if supplied_headlines is None and not current_headlines:
            raise ValueError(
                "Could not read the ad's current headlines to preserve them; "
                "supply headlines explicitly."
            )
        if supplied_descriptions is None and not current_descriptions:
            raise ValueError(
                "Could not read the ad's current descriptions to preserve them; "
                "supply descriptions explicitly."
            )
        effective_headlines = (
            supplied_headlines if supplied_headlines is not None else current_headlines
        )
        effective_descriptions = (
            supplied_descriptions
            if supplied_descriptions is not None
            else current_descriptions
        )

        # Use dummy URL for validation when final_url is not specified (URL itself is not updated)
        validation_url = final_url if final_url else "https://placeholder.example.com"
        headlines, descriptions, rsa_result = self._validate_and_prepare_rsa(
            effective_headlines,
            effective_descriptions,
            validation_url,
        )

        # Update via AdService (not AdGroupAdService)
        ad_service = self._get_service("AdService")
        op = self._client.get_type("AdOperation")
        ad = op.update
        ad.resource_name = ad_service.ad_path(self._customer_id, ad_id)

        # Only mask the side(s) the caller actually supplied; the other side
        # keeps its current assets untouched.
        paths: list[str] = []
        if supplied_headlines is not None:
            for h in headlines:
                text_asset = self._client.get_type("AdTextAsset")
                text_asset.text = h
                ad.responsive_search_ad.headlines.append(text_asset)
            paths.append("responsive_search_ad.headlines")
        if supplied_descriptions is not None:
            for d in descriptions:
                text_asset = self._client.get_type("AdTextAsset")
                text_asset.text = d
                ad.responsive_search_ad.descriptions.append(text_asset)
            paths.append("responsive_search_ad.descriptions")
        if final_url:
            ad.final_urls.append(final_url)
            paths.append("final_urls")
        self._client.copy_from(op.update_mask, PbFieldMask(paths=paths))

        response = ad_service.mutate_ads(
            customer_id=self._customer_id,
            operations=[op],
        )
        result: dict[str, Any] = {
            "resource_name": response.results[0].resource_name,
        }
        return self._build_ad_strength_result(
            result,
            rsa_result,
            headlines,
            descriptions,
            params.get("keywords"),
        )

    _MAX_ENABLED_RSA_PER_AD_GROUP = 3

    @_wrap_mutate_error("ad status change")
    async def update_ad_status(
        self, ad_group_id: str, ad_id: str, status: str
    ) -> dict[str, Any]:
        """Change ad status."""
        self._validate_id(ad_group_id, "ad_group_id")
        self._validate_id(ad_id, "ad_id")
        validated_status = self._validate_status(status)

        # Check RSA limit when changing to ENABLED
        if validated_status == "ENABLED":
            try:
                ads_data = await self.list_ads(ad_group_id=ad_group_id)
                ads = ads_data.get("ads", []) if isinstance(ads_data, dict) else []  # type: ignore[var-annotated]
                enabled_rsa = sum(
                    1  # type: ignore[misc]
                    for a in ads
                    if a.get("status") == "ENABLED"
                    and a.get("type") == "RESPONSIVE_SEARCH_AD"
                    and str(a.get("id", "")) != ad_id
                )
                if enabled_rsa >= self._MAX_ENABLED_RSA_PER_AD_GROUP:
                    return {
                        "error": True,
                        "error_type": "validation_error",
                        "message": (
                            f"This ad group already has {enabled_rsa} active RSAs"
                            f" (limit: {self._MAX_ENABLED_RSA_PER_AD_GROUP})."
                            " Please pause existing ads before enabling."
                        ),
                    }
            except Exception:
                logger.debug("RSA limit check failed (continuing)", exc_info=True)

        ad_group_ad_service = self._get_service("AdGroupAdService")
        op = self._client.get_type("AdGroupAdOperation")
        ad_group_ad = op.update
        ad_group_ad.resource_name = self._client.get_service(
            "AdGroupAdService"
        ).ad_group_ad_path(self._customer_id, ad_group_id, ad_id)
        status_enum = self._client.enums.AdGroupAdStatusEnum
        ad_group_ad.status = getattr(status_enum, validated_status)
        self._client.copy_from(
            op.update_mask,
            PbFieldMask(paths=["status"]),
        )
        response = ad_group_ad_service.mutate_ad_group_ads(
            customer_id=self._customer_id,
            operations=[op],
        )
        return {"resource_name": response.results[0].resource_name}
