"""
Instantly.ai sequencer provider implementation.

API Reference: https://developer.instantly.ai/api/v2
"""

from __future__ import annotations

from datetime import datetime
from typing import Any, Dict, List, Optional

from g_gremlin.sequencer import (
    SequencerAuthError,
    SequencerError,
    SequencerRateLimitError,
)
from g_gremlin.sequencer.models import (
    INSTANTLY_RATE_LIMIT,
    BatchOutcome,
    Campaign,
    EnrollmentResult,
    NormalizedEvent,
    Prospect,
    RateLimitConfig,
)
from g_gremlin.sequencer.provider import BaseProvider, WebhookRequest


# Event type mapping from Instantly to normalized types
INSTANTLY_EVENT_MAP = {
    "email_sent": "sent",
    "email_opened": "opened",
    "link_clicked": "clicked",
    "reply_received": "replied",
    "auto_reply_received": "auto_replied",
    "email_bounced": "bounced",
    "lead_unsubscribed": "unsubscribed",
    "lead_meeting_booked": "meeting_booked",
    "lead_meeting_completed": "meeting_booked",
    "lead_out_of_office": "out_of_office",
    "lead_interested": "category_updated",
    "lead_not_interested": "category_updated",
    "lead_neutral": "category_updated",
    "lead_closed": "category_updated",
    "lead_wrong_person": "category_updated",
}


class InstantlyProvider(BaseProvider):
    """
    Instantly.ai sequencer provider.

    Implements the SequencerProvider protocol for Instantly V2 API.
    """

    BASE_URL = "https://api.instantly.ai/api/v2"

    def __init__(
        self,
        api_key: str,
        timeout: int = 30,
        base_url: Optional[str] = None,
        use_bearer_auth: bool = False,  # Accepted for interface consistency; Instantly always uses Bearer
    ):
        super().__init__(
            api_key=api_key,
            base_url=base_url or self.BASE_URL,
            timeout=timeout,
        )
        # use_bearer_auth is ignored - Instantly always uses Bearer token authentication

    @property
    def name(self) -> str:
        return "instantly"

    @property
    def rate_limit(self) -> RateLimitConfig:
        return INSTANTLY_RATE_LIMIT

    def _headers(self) -> Dict[str, str]:
        """Instantly uses Bearer token authentication."""
        return {
            "Authorization": f"Bearer {self.api_key}",
            "Content-Type": "application/json",
            "Accept": "application/json",
        }

    def _request(
        self,
        method: str,
        endpoint: str,
        json: Optional[Dict] = None,
        params: Optional[Dict] = None,
    ) -> Dict[str, Any]:
        """Make authenticated request to Instantly API."""
        session = self._get_session()
        url = f"{self.base_url}/{endpoint.lstrip('/')}"

        resp = session.request(
            method=method,
            url=url,
            headers=self._headers(),
            json=json,
            params=params,
            timeout=self.timeout,
        )

        if resp.status_code == 401:
            raise SequencerAuthError("Instantly API key is invalid or expired")
        if resp.status_code == 429:
            raise SequencerRateLimitError("Instantly rate limit exceeded")
        if resp.status_code >= 400:
            try:
                error_detail = resp.json()
            except Exception:
                error_detail = resp.text
            raise SequencerError(f"Instantly API error {resp.status_code}: {error_detail}")

        return resp.json() if resp.content else {}

    def _prospect_to_instantly(self, prospect: Prospect) -> Dict:
        """Convert Prospect to Instantly lead format."""
        lead = {
            "email": prospect.email,
        }
        if prospect.first_name:
            lead["first_name"] = prospect.first_name
        if prospect.last_name:
            lead["last_name"] = prospect.last_name
        if prospect.company:
            lead["company_name"] = prospect.company
        if prospect.title:
            lead["title"] = prospect.title
        if prospect.phone:
            lead["phone"] = prospect.phone
        if prospect.linkedin_url:
            lead["linkedin_url"] = prospect.linkedin_url
        if prospect.domain:
            lead["website"] = prospect.domain

        # Add custom variables
        if prospect.custom_vars:
            lead["custom_variables"] = {
                k: str(v) if v is not None else None
                for k, v in prospect.custom_vars.items()
            }

        return lead

    def _bulk_summary(self, resp: Dict[str, Any]) -> str:
        parts = []
        for key, label in (
            ("duplicated_leads", "duplicate in campaign/list"),
            ("skipped_count", "skipped by flags"),
            ("invalid_email_count", "invalid email"),
            ("in_blocklist", "in blocklist"),
            ("duplicate_email_count", "duplicate in payload"),
            ("incomplete_count", "missing required fields"),
        ):
            try:
                count = int(resp.get(key) or 0)
            except (TypeError, ValueError):
                count = 0
            if count:
                parts.append(f"{label}: {count}")
        return "Instantly did not create lead (" + ", ".join(parts) + ")." if parts else "Instantly did not create lead."

    def _parse_bulk_add(self, resp: Any) -> tuple[Dict[int, Dict[str, Any]], Dict[str, Any]]:
        if not isinstance(resp, dict):
            return {}, {}
        created = resp.get("created_leads") or []
        created_map: Dict[int, Dict[str, Any]] = {}
        if isinstance(created, list):
            for item in created:
                try:
                    idx = int(item.get("index"))
                except (TypeError, ValueError, AttributeError):
                    continue
                created_map[idx] = item
        return created_map, resp

    def upsert_prospects(
        self,
        prospects: List[Prospect],
        campaign_id: Optional[str] = None,
    ) -> BatchOutcome:
        """
        Create or update prospects in Instantly, optionally enrolling in campaign.

        Uses bulk add endpoint for efficiency.
        """
        outcome = BatchOutcome()

        if not prospects:
            return outcome

        # Convert to Instantly format
        leads = [self._prospect_to_instantly(p) for p in prospects]

        try:
            payload = {
                "leads": leads,
            }
            if campaign_id:
                payload["campaign_id"] = campaign_id
                payload["skip_if_in_campaign"] = True  # Avoid duplicates
            resp = self._request("POST", "/leads/add", json=payload)
            outcome.last_status_code = 200

            # Process response
            created_map, summary = self._parse_bulk_add(resp)
            missing_indices = []

            def count(key: str) -> int:
                try:
                    return int(summary.get(key) or 0)
                except (TypeError, ValueError):
                    return 0

            leads_uploaded = count("leads_uploaded")
            if not created_map and leads_uploaded:
                for i in range(min(leads_uploaded, len(prospects))):
                    created_map[i] = {}

            for i, prospect in enumerate(prospects):
                lead_data = created_map.get(i)
                if lead_data:
                    outcome.succeeded.append(EnrollmentResult(
                        email=prospect.email,
                        campaign_id=campaign_id or "",
                        status="enrolled",
                        provider_prospect_id=lead_data.get("id"),
                    ))
                else:
                    missing_indices.append(i)

            if missing_indices:
                missing_count = len(missing_indices)
                invalid_count = count("invalid_email_count")
                blocklist_count = count("in_blocklist")
                incomplete_count = count("incomplete_count")
                duplicate_payload_count = count("duplicate_email_count")
                duplicated_count = count("duplicated_leads")
                skipped_count = count("skipped_count")

                bad_sum = invalid_count + blocklist_count + incomplete_count + duplicate_payload_count
                exists_sum = duplicated_count + skipped_count

                if bad_sum == 0 and exists_sum == missing_count and exists_sum > 0:
                    status = "skipped"
                    error = None
                elif exists_sum == 0 and bad_sum == missing_count and bad_sum > 0:
                    status = "failed"
                    error = self._bulk_summary(summary)
                else:
                    status = "failed"
                    error = self._bulk_summary(summary)

                for idx in missing_indices:
                    prospect = prospects[idx]
                    target = outcome.skipped if status == "skipped" else outcome.failed
                    target.append(EnrollmentResult(
                        email=prospect.email,
                        campaign_id=campaign_id or "",
                        status=status,
                        error=error,
                    ))

        except SequencerRateLimitError:
            outcome.last_status_code = 429
            # Mark all as failed due to rate limit
            for prospect in prospects:
                outcome.failed.append(EnrollmentResult(
                    email=prospect.email,
                    campaign_id=campaign_id or "",
                    status="failed",
                    error="Rate limit exceeded",
                ))
            raise

        except SequencerError as e:
            # Mark all as failed
            for prospect in prospects:
                outcome.failed.append(EnrollmentResult(
                    email=prospect.email,
                    campaign_id=campaign_id or "",
                    status="failed",
                    error=str(e),
                ))

        return outcome

    def enroll_in_campaign(
        self,
        prospect_ids: List[str],
        campaign_id: str,
    ) -> BatchOutcome:
        """Enroll existing prospects in a campaign by their Instantly IDs."""
        outcome = BatchOutcome()

        try:
            # Move leads to campaign
            payload = {
                "lead_ids": prospect_ids,
                "to_campaign_id": campaign_id,
            }
            resp = self._request("POST", "/leads/move", json=payload)
            outcome.last_status_code = 200

            # Process results
            for lead_id in prospect_ids:
                outcome.succeeded.append(EnrollmentResult(
                    email="",  # We don't have email from just IDs
                    campaign_id=campaign_id,
                    status="enrolled",
                    provider_prospect_id=lead_id,
                ))

        except SequencerError as e:
            for lead_id in prospect_ids:
                outcome.failed.append(EnrollmentResult(
                    email="",
                    campaign_id=campaign_id,
                    status="failed",
                    provider_prospect_id=lead_id,
                    error=str(e),
                ))

        return outcome

    def list_campaigns(self) -> List[Campaign]:
        """List all Instantly campaigns."""
        campaigns = []
        skip = 0
        limit = 100

        while True:
            resp = self._request("GET", "/campaigns", params={"skip": skip, "limit": limit})
            items = resp.get("items", resp) if isinstance(resp, dict) else resp

            if not items:
                break

            for item in items:
                campaigns.append(Campaign(
                    id=item.get("id", ""),
                    name=item.get("name", ""),
                    status=item.get("status"),
                    metadata={
                        "created_at": item.get("created_at"),
                        "updated_at": item.get("updated_at"),
                    },
                ))

            if len(items) < limit:
                break
            skip += limit

        return campaigns

    def create_campaign(self, name: str, timezone: str = "America/New_York", **kwargs) -> Campaign:
        """
        Create a new Instantly campaign.

        Args:
            name: Campaign name
            timezone: Timezone for the campaign schedule (default: America/New_York)

        Returns:
            The created Campaign object
        """
        payload = {
            "name": name,
            "campaign_schedule": {
                "schedules": [{
                    "name": "Default Schedule",
                    "timing": {"from": "09:00", "to": "17:00"},
                    "days": {"1": True, "2": True, "3": True, "4": True, "5": True},
                    "timezone": timezone,
                }]
            }
        }

        resp = self._request("POST", "/campaigns", json=payload)

        return Campaign(
            id=resp.get("id", ""),
            name=resp.get("name", name),
            status="active",
            metadata={
                "created_at": resp.get("created_at"),
            },
        )

    def list_email_accounts(self) -> List[Dict[str, Any]]:
        """List Instantly sending accounts."""
        endpoints = ["/accounts", "/sending-accounts", "/email-accounts"]
        last_error: Optional[Exception] = None

        for endpoint in endpoints:
            try:
                resp = self._request("GET", endpoint)
            except SequencerError as exc:
                last_error = exc
                if "404" in str(exc):
                    continue
                raise

            items = resp
            if isinstance(resp, dict):
                items = resp.get("items") or resp.get("data") or resp.get("accounts") or []
            if not isinstance(items, list):
                continue

            accounts: List[Dict[str, Any]] = []
            for item in items:
                if not isinstance(item, dict):
                    continue
                accounts.append({
                    "id": item.get("id") or item.get("account_id"),
                    "email": item.get("email") or item.get("email_address"),
                    "name": item.get("name") or item.get("sender_name") or item.get("from_name"),
                    "status": item.get("status") or item.get("state"),
                })
            return accounts

        if last_error and "404" not in str(last_error):
            raise last_error
        return []

    def verify_webhook(self, request: WebhookRequest) -> bool:
        """
        Instantly does not provide webhook signatures.

        Always returns True. Users should rely on network-level security
        (IP allowlisting, Cloudflare Access, etc.).
        """
        # Instantly has no signature verification
        # Recommend network-level allowlisting
        return True

    def normalize_event(self, raw_event: Dict[str, Any]) -> NormalizedEvent:
        """Convert Instantly webhook payload to normalized event."""
        event_type_raw = raw_event.get("event_type", "")
        event_type = INSTANTLY_EVENT_MAP.get(event_type_raw, "category_updated")

        # Parse timestamp
        timestamp_str = raw_event.get("timestamp", "")
        try:
            timestamp = datetime.fromisoformat(timestamp_str.replace("Z", "+00:00"))
        except (ValueError, AttributeError):
            timestamp = datetime.utcnow()

        # Build normalized event
        event = NormalizedEvent(
            event_type=event_type,
            timestamp=timestamp,
            provider="instantly",
            provider_event_id=raw_event.get("email_id", raw_event.get("id", "")),
            email=raw_event.get("lead_email", ""),
            campaign_id=raw_event.get("campaign_id", ""),
            campaign_name=raw_event.get("campaign_name"),
            raw_payload=raw_event,
        )

        # Add reply-specific fields
        if event_type in ("replied", "auto_replied"):
            event.reply_text = raw_event.get("reply_text")
            event.reply_subject = raw_event.get("reply_subject")
            event.reply_html = raw_event.get("reply_html")

        # Add category for label events
        if event_type == "category_updated":
            event.category = event_type_raw.replace("lead_", "")

        return event

    def test_connection(self) -> Dict[str, Any]:
        """Test API connectivity and return account info."""
        try:
            # List campaigns to verify credentials (most reliable endpoint)
            campaigns = self.list_campaigns()

            return {
                "status": "ok",
                "provider": "instantly",
                "campaign_count": len(campaigns),
                "webhooks_available": True,  # Webhooks available on Hyper Growth+
            }
        except SequencerAuthError:
            return {
                "status": "error",
                "provider": "instantly",
                "error": "Authentication failed - check API key",
            }
        except SequencerError as e:
            return {
                "status": "error",
                "provider": "instantly",
                "error": str(e),
            }
