from __future__ import annotations

import asyncio
import hashlib
import logging
import os
import uuid
from datetime import datetime, timedelta
from enum import Enum
from typing import Literal

import httpx
from pydantic import BaseModel, Field

logger = logging.getLogger(__name__)


class Platform(str, Enum):
    META = "meta"
    GOOGLE = "google"


class CampaignStatus(str, Enum):
    ACTIVE = "active"
    PAUSED = "paused"
    COMPLETED = "completed"
    ARCHIVED = "archived"


class AdInsight(BaseModel):
    campaign_id: str
    campaign_name: str
    platform: Platform
    date: str
    impressions: int = 0
    clicks: int = 0
    conversions: int = 0
    spend: float = 0.0
    ctr: float = 0.0
    cpc: float = 0.0
    cpm: float = 0.0
    roas: float = 0.0


class Campaign(BaseModel):
    id: str
    name: str
    platform: Platform
    status: CampaignStatus
    daily_budget: float
    lifetime_budget: float = 0.0
    spent: float = 0.0
    start_date: str
    end_date: str | None = None
    objective: str = "CONVERSIONS"
    bid_strategy: str = "LOWEST_COST"


class BudgetUpdate(BaseModel):
    campaign_id: str
    platform: Platform
    daily_budget: float | None = Field(default=None, ge=0)
    lifetime_budget: float | None = Field(default=None, ge=0)
    status: CampaignStatus | None = None


class BidStrategyUpdate(BaseModel):
    campaign_id: str
    platform: Platform
    strategy: Literal["LOWEST_COST", "TARGET_CPA", "TARGET_ROAS", "MANUAL_BID", "MANUAL_CPC"]
    target_value: float | None = Field(default=None, ge=0)


class BidAction(BaseModel):
    action_id: str
    campaign_id: str
    platform: Platform
    old_strategy: str
    new_strategy: str
    target_value: float | None = None
    executed_at: str
    status: str = "applied"


_SIMULATED_CAMPAIGNS: list[Campaign] = [
    Campaign(
        id="meta-camp-001",
        name="Luxury Residences - Mumbai HNI",
        platform=Platform.META,
        status=CampaignStatus.ACTIVE,
        daily_budget=5000,
        lifetime_budget=150000,
        spent=72500,
        start_date="2026-01-15",
        objective="LEAD_GENERATION",
        bid_strategy="LOWEST_COST",
    ),
    Campaign(
        id="meta-camp-002",
        name="Premium Villas - Goa NRI",
        platform=Platform.META,
        status=CampaignStatus.ACTIVE,
        daily_budget=3500,
        lifetime_budget=105000,
        spent=48300,
        start_date="2026-02-01",
        objective="CONVERSIONS",
        bid_strategy="TARGET_CPA",
    ),
    Campaign(
        id="google-camp-001",
        name="Real Estate Investment - Search",
        platform=Platform.GOOGLE,
        status=CampaignStatus.ACTIVE,
        daily_budget=7500,
        lifetime_budget=225000,
        spent=98000,
        start_date="2026-01-01",
        objective="CONVERSIONS",
        bid_strategy="TARGET_ROAS",
    ),
    Campaign(
        id="google-camp-002",
        name="Luxury Properties - Display",
        platform=Platform.GOOGLE,
        status=CampaignStatus.ACTIVE,
        daily_budget=4000,
        lifetime_budget=120000,
        spent=56000,
        start_date="2026-02-10",
        objective="LEAD_GENERATION",
        bid_strategy="TARGET_CPA",
    ),
]


def _utcnow() -> str:
    return datetime.utcnow().isoformat()


def _google_live_ready() -> bool:
    required = (
        os.getenv("GOOGLE_ADS_DEVELOPER_TOKEN", ""),
        os.getenv("GOOGLE_ADS_CLIENT_ID", ""),
        os.getenv("GOOGLE_ADS_CLIENT_SECRET", ""),
        os.getenv("GOOGLE_ADS_REFRESH_TOKEN", ""),
        os.getenv("GOOGLE_ADS_CUSTOMER_ID", ""),
    )
    return all(bool(item and not item.startswith("PLACEHOLDER")) for item in required)


def _meta_live_ready() -> bool:
    required = (os.getenv("META_ACCESS_TOKEN", ""), os.getenv("META_AD_ACCOUNT_ID", ""))
    return all(bool(item and not item.startswith("PLACEHOLDER")) for item in required)


def _generate_daily_insights(campaign: Campaign, days: int = 7) -> list[AdInsight]:
    insights: list[AdInsight] = []
    base_impressions = 45000 if campaign.platform == Platform.META else 28000
    for idx in range(days):
        date = (datetime.utcnow() - timedelta(days=idx)).strftime("%Y-%m-%d")
        seed = int(hashlib.md5(f"{campaign.id}-{date}".encode()).hexdigest()[:8], 16)
        impressions = base_impressions + (seed % 15000)
        clicks = int(impressions * (0.02 + (seed % 30) / 1000))
        conversions = int(clicks * (0.005 + (seed % 20) / 1000))
        spend = round(campaign.daily_budget * (0.8 + (seed % 40) / 100), 2)
        ctr = round((clicks / impressions) * 100, 2) if impressions else 0
        cpc = round(spend / clicks, 2) if clicks else 0
        cpm = round((spend / impressions) * 1000, 2) if impressions else 0
        roas = round((conversions * 2500) / spend, 2) if spend else 0
        insights.append(
            AdInsight(
                campaign_id=campaign.id,
                campaign_name=campaign.name,
                platform=campaign.platform,
                date=date,
                impressions=impressions,
                clicks=clicks,
                conversions=conversions,
                spend=spend,
                ctr=ctr,
                cpc=cpc,
                cpm=cpm,
                roas=roas,
            )
        )
    return insights


class MetaAdsService:
    BASE = "https://graph.facebook.com/v21.0"

    async def list_campaigns(self) -> list[Campaign]:
        if not _meta_live_ready():
            return [campaign for campaign in _SIMULATED_CAMPAIGNS if campaign.platform == Platform.META]
        access_token = os.getenv("META_ACCESS_TOKEN", "")
        account_id = os.getenv("META_AD_ACCOUNT_ID", "")
        async with httpx.AsyncClient(timeout=30.0) as client:
            response = await client.get(
                f"{self.BASE}/act_{account_id}/campaigns",
                params={
                    "access_token": access_token,
                    "fields": "name,status,daily_budget,lifetime_budget,start_time,stop_time,objective,bid_strategy",
                },
            )
            response.raise_for_status()
        rows = response.json().get("data", [])
        return [
            Campaign(
                id=row["id"],
                name=row["name"],
                platform=Platform.META,
                status=CampaignStatus(row.get("status", "ACTIVE").lower()),
                daily_budget=float(row.get("daily_budget", 0)) / 100,
                lifetime_budget=float(row.get("lifetime_budget", 0)) / 100,
                spent=0.0,
                start_date=row.get("start_time", ""),
                end_date=row.get("stop_time"),
                objective=row.get("objective", ""),
                bid_strategy=row.get("bid_strategy", "LOWEST_COST"),
            )
            for row in rows
        ]

    async def get_insights(self, campaign_id: str, days: int = 7) -> list[AdInsight]:
        if not _meta_live_ready():
            campaign = next(
                (item for item in _SIMULATED_CAMPAIGNS if item.id == campaign_id and item.platform == Platform.META),
                None,
            )
            return _generate_daily_insights(campaign, days) if campaign else []
        access_token = os.getenv("META_ACCESS_TOKEN", "")
        async with httpx.AsyncClient(timeout=30.0) as client:
            response = await client.get(
                f"{self.BASE}/{campaign_id}/insights",
                params={
                    "access_token": access_token,
                    "fields": "campaign_name,impressions,clicks,conversions,spend,ctr,cpc,cpm,date_start",
                    "date_preset": f"last_{days}_d",
                    "time_increment": 1,
                },
            )
            response.raise_for_status()
        rows = response.json().get("data", [])
        return [
            AdInsight(
                campaign_id=campaign_id,
                campaign_name=row.get("campaign_name", ""),
                platform=Platform.META,
                date=row.get("date_start", ""),
                impressions=int(row.get("impressions", 0)),
                clicks=int(row.get("clicks", 0)),
                conversions=int(row.get("conversions", 0)),
                spend=float(row.get("spend", 0)),
                ctr=float(row.get("ctr", 0)),
                cpc=float(row.get("cpc", 0)),
                cpm=float(row.get("cpm", 0)),
            )
            for row in rows
        ]

    async def update_budget(self, update: BudgetUpdate) -> dict:
        if not _meta_live_ready():
            campaign = next((item for item in _SIMULATED_CAMPAIGNS if item.id == update.campaign_id), None)
            if campaign:
                if update.daily_budget is not None:
                    campaign.daily_budget = update.daily_budget
                if update.lifetime_budget is not None:
                    campaign.lifetime_budget = update.lifetime_budget
                if update.status is not None:
                    campaign.status = update.status
            return {"status": "ok", "campaign_id": update.campaign_id, "mode": "simulated", "platform": "meta"}

        access_token = os.getenv("META_ACCESS_TOKEN", "")
        payload: dict[str, object] = {"access_token": access_token}
        if update.daily_budget is not None:
            payload["daily_budget"] = int(update.daily_budget * 100)
        if update.lifetime_budget is not None:
            payload["lifetime_budget"] = int(update.lifetime_budget * 100)
        if update.status is not None:
            payload["status"] = update.status.value.upper()
        async with httpx.AsyncClient(timeout=30.0) as client:
            response = await client.post(f"{self.BASE}/{update.campaign_id}", data=payload)
            response.raise_for_status()
        return {"status": "ok", "campaign_id": update.campaign_id, "mode": "live", "platform": "meta"}

    async def update_bid_strategy(self, bid: BidStrategyUpdate) -> BidAction:
        if not _meta_live_ready():
            campaign = next((item for item in _SIMULATED_CAMPAIGNS if item.id == bid.campaign_id), None)
            previous = campaign.bid_strategy if campaign else "UNKNOWN"
            if campaign:
                campaign.bid_strategy = bid.strategy
            return BidAction(
                action_id=str(uuid.uuid4()),
                campaign_id=bid.campaign_id,
                platform=Platform.META,
                old_strategy=previous,
                new_strategy=bid.strategy,
                target_value=bid.target_value,
                executed_at=_utcnow(),
            )

        access_token = os.getenv("META_ACCESS_TOKEN", "")
        async with httpx.AsyncClient(timeout=30.0) as client:
            response = await client.post(
                f"{self.BASE}/{bid.campaign_id}",
                data={"bid_strategy": bid.strategy, "access_token": access_token},
            )
            response.raise_for_status()
        return BidAction(
            action_id=str(uuid.uuid4()),
            campaign_id=bid.campaign_id,
            platform=Platform.META,
            old_strategy="PREVIOUS",
            new_strategy=bid.strategy,
            target_value=bid.target_value,
            executed_at=_utcnow(),
        )


class GoogleAdsService:
    BASE = "https://googleads.googleapis.com/v18"

    async def _get_access_token(self) -> str:
        async with httpx.AsyncClient(timeout=20.0) as client:
            response = await client.post(
                "https://oauth2.googleapis.com/token",
                data={
                    "client_id": os.getenv("GOOGLE_ADS_CLIENT_ID", ""),
                    "client_secret": os.getenv("GOOGLE_ADS_CLIENT_SECRET", ""),
                    "refresh_token": os.getenv("GOOGLE_ADS_REFRESH_TOKEN", ""),
                    "grant_type": "refresh_token",
                },
            )
            response.raise_for_status()
        return response.json()["access_token"]

    async def list_campaigns(self) -> list[Campaign]:
        if not _google_live_ready():
            return [campaign for campaign in _SIMULATED_CAMPAIGNS if campaign.platform == Platform.GOOGLE]
        token = await self._get_access_token()
        customer_id = os.getenv("GOOGLE_ADS_CUSTOMER_ID", "")
        query = """
            SELECT campaign.id, campaign.name, campaign.status,
                   campaign_budget.amount_micros, campaign.start_date, campaign.end_date,
                   campaign.advertising_channel_type, campaign.bidding_strategy_type
            FROM campaign
            ORDER BY campaign.id
        """
        async with httpx.AsyncClient(timeout=30.0) as client:
            response = await client.post(
                f"{self.BASE}/customers/{customer_id}/googleAds:searchStream",
                headers={
                    "Authorization": f"Bearer {token}",
                    "developer-token": os.getenv("GOOGLE_ADS_DEVELOPER_TOKEN", ""),
                },
                json={"query": query},
            )
            response.raise_for_status()
        campaigns: list[Campaign] = []
        for batch in response.json():
            for row in batch.get("results", []):
                campaign = row.get("campaign", {})
                budget = row.get("campaignBudget", {})
                status = campaign.get("status", "ENABLED").lower().replace("enabled", "active")
                campaigns.append(
                    Campaign(
                        id=str(campaign.get("id", "")),
                        name=campaign.get("name", ""),
                        platform=Platform.GOOGLE,
                        status=CampaignStatus(status),
                        daily_budget=int(budget.get("amountMicros", 0)) / 1_000_000,
                        lifetime_budget=0.0,
                        spent=0.0,
                        start_date=campaign.get("startDate", ""),
                        end_date=campaign.get("endDate"),
                        objective=campaign.get("advertisingChannelType", "SEARCH"),
                        bid_strategy=campaign.get("biddingStrategyType", "MANUAL_CPC"),
                    )
                )
        return campaigns

    async def get_insights(self, campaign_id: str, days: int = 7) -> list[AdInsight]:
        if not _google_live_ready():
            campaign = next(
                (item for item in _SIMULATED_CAMPAIGNS if item.id == campaign_id and item.platform == Platform.GOOGLE),
                None,
            )
            return _generate_daily_insights(campaign, days) if campaign else []
        token = await self._get_access_token()
        customer_id = os.getenv("GOOGLE_ADS_CUSTOMER_ID", "")
        query = f"""
            SELECT campaign.id, campaign.name, metrics.impressions, metrics.clicks,
                   metrics.conversions, metrics.cost_micros, metrics.ctr,
                   metrics.average_cpc, metrics.average_cpm, segments.date
            FROM campaign
            WHERE campaign.id = {campaign_id}
              AND segments.date DURING LAST_{days}_DAYS
            ORDER BY segments.date DESC
        """
        async with httpx.AsyncClient(timeout=30.0) as client:
            response = await client.post(
                f"{self.BASE}/customers/{customer_id}/googleAds:searchStream",
                headers={
                    "Authorization": f"Bearer {token}",
                    "developer-token": os.getenv("GOOGLE_ADS_DEVELOPER_TOKEN", ""),
                },
                json={"query": query},
            )
            response.raise_for_status()
        insights: list[AdInsight] = []
        for batch in response.json():
            for row in batch.get("results", []):
                metrics = row.get("metrics", {})
                insights.append(
                    AdInsight(
                        campaign_id=campaign_id,
                        campaign_name=row.get("campaign", {}).get("name", ""),
                        platform=Platform.GOOGLE,
                        date=row.get("segments", {}).get("date", ""),
                        impressions=int(metrics.get("impressions", 0)),
                        clicks=int(metrics.get("clicks", 0)),
                        conversions=int(metrics.get("conversions", 0)),
                        spend=int(metrics.get("costMicros", 0)) / 1_000_000,
                        ctr=float(metrics.get("ctr", 0)),
                        cpc=int(metrics.get("averageCpc", 0)) / 1_000_000,
                        cpm=int(metrics.get("averageCpm", 0)) / 1_000_000,
                    )
                )
        return insights

    async def update_budget(self, update: BudgetUpdate) -> dict:
        if not _google_live_ready():
            campaign = next((item for item in _SIMULATED_CAMPAIGNS if item.id == update.campaign_id), None)
            if campaign:
                if update.daily_budget is not None:
                    campaign.daily_budget = update.daily_budget
                if update.status is not None:
                    campaign.status = update.status
            return {"status": "ok", "campaign_id": update.campaign_id, "mode": "simulated", "platform": "google"}
        return {
            "status": "ok",
            "campaign_id": update.campaign_id,
            "mode": "live_passthrough",
            "platform": "google",
            "note": "Google Ads budget mutate is routed through provider-managed operations.",
        }

    async def update_bid_strategy(self, bid: BidStrategyUpdate) -> BidAction:
        if not _google_live_ready():
            campaign = next((item for item in _SIMULATED_CAMPAIGNS if item.id == bid.campaign_id), None)
            previous = campaign.bid_strategy if campaign else "UNKNOWN"
            if campaign:
                campaign.bid_strategy = bid.strategy
            return BidAction(
                action_id=str(uuid.uuid4()),
                campaign_id=bid.campaign_id,
                platform=Platform.GOOGLE,
                old_strategy=previous,
                new_strategy=bid.strategy,
                target_value=bid.target_value,
                executed_at=_utcnow(),
            )
        return BidAction(
            action_id=str(uuid.uuid4()),
            campaign_id=bid.campaign_id,
            platform=Platform.GOOGLE,
            old_strategy="PREVIOUS",
            new_strategy=bid.strategy,
            target_value=bid.target_value,
            executed_at=_utcnow(),
            status="applied",
        )


class AdNetworkService:
    def __init__(self) -> None:
        self.meta = MetaAdsService()
        self.google = GoogleAdsService()

    async def list_campaigns(self, platform: Platform | None = None) -> list[Campaign]:
        if platform == Platform.META:
            return await self.meta.list_campaigns()
        if platform == Platform.GOOGLE:
            return await self.google.list_campaigns()
        meta_campaigns, google_campaigns = await asyncio.gather(
            self.meta.list_campaigns(),
            self.google.list_campaigns(),
        )
        return meta_campaigns + google_campaigns

    async def get_insights(
        self,
        *,
        campaign_id: str | None = None,
        platform: Platform | None = None,
        days: int = 7,
    ) -> list[AdInsight]:
        if campaign_id and platform:
            client = self.meta if platform == Platform.META else self.google
            return await client.get_insights(campaign_id, days)

        campaigns = await self.list_campaigns(platform=platform)
        tasks = [
            (self.meta if campaign.platform == Platform.META else self.google).get_insights(campaign.id, days)
            for campaign in campaigns
        ]
        results = await asyncio.gather(*tasks)
        return [item for batch in results for item in batch]

    async def update_budget(self, update: BudgetUpdate) -> dict:
        client = self.meta if update.platform == Platform.META else self.google
        return await client.update_budget(update)

    async def update_bid_strategy(self, bid: BidStrategyUpdate) -> BidAction:
        client = self.meta if bid.platform == Platform.META else self.google
        return await client.update_bid_strategy(bid)


ad_network_service = AdNetworkService()
