"""
Apollo.io sequencer provider implementation.

Supports Apollo's email sequences API for enrolling contacts in automated outreach campaigns.

API Reference: https://apolloio.github.io/apollo-api-docs/
"""

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 (
    APOLLO_RATE_LIMIT,
    BatchOutcome,
    Campaign,
    EnrollmentResult,
    NormalizedEvent,
    Prospect,
    RateLimitConfig,
)
from g_gremlin.sequencer.provider import BaseProvider, WebhookRequest


# Event type mapping from Apollo to normalized types
APOLLO_EVENT_MAP = {
    "email_sent": "sent",
    "email_opened": "opened",
    "email_clicked": "clicked",
    "email_replied": "replied",
    "email_bounced": "bounced",
    "email_unsubscribed": "unsubscribed",
    "email_auto_replied": "auto_replied",
    "contact_replied": "replied",
    "contact_interested": "category_updated",
    "contact_not_interested": "category_updated",
}


class ApolloSequencerProvider(BaseProvider):
    """
    Apollo.io sequencer provider.

    Implements the SequencerProvider protocol for Apollo's email sequences API.
    Reuses credentials from the existing Apollo enrichment integration.
    """

    BASE_URL = "https://api.apollo.io"

    def __init__(
        self,
        api_key: str,
        timeout: int = 30,
        base_url: Optional[str] = None,
        use_bearer_auth: bool = False,
        send_email_from_email_account_id: Optional[str] = None,
    ):
        super().__init__(
            api_key=api_key,
            base_url=base_url or self.BASE_URL,
            timeout=timeout,
        )
        self.use_bearer_auth = use_bearer_auth
        self.send_email_from_email_account_id = (
            send_email_from_email_account_id.strip()
            if send_email_from_email_account_id
            else None
        )

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

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

    def _headers(self) -> Dict[str, str]:
        """Apollo auth headers. OAuth uses Bearer token, API keys use x-api-key."""
        headers = {
            "Content-Type": "application/json",
            "Accept": "application/json",
        }
        if self.use_bearer_auth:
            headers["Authorization"] = f"Bearer {self.api_key}"
        else:
            headers["x-api-key"] = self.api_key
        return headers

    def _request(
        self,
        method: str,
        endpoint: str,
        json: Optional[Dict] = None,
        params: Optional[Dict] = None,
    ) -> Dict[str, Any]:
        """Make authenticated request to Apollo API."""
        import logging
        logger = logging.getLogger(__name__)

        session = self._get_session()
        url = f"{self.base_url}/{endpoint.lstrip('/')}"

        logger.info(f"[APOLLO] {method} {url} payload={json} params={params}")

        resp = session.request(
            method=method,
            url=url,
            headers=self._headers(),
            json=json if method.upper() != "GET" else None,
            params=params,
            timeout=self.timeout,
        )

        logger.info(f"[APOLLO] Response {resp.status_code}: {resp.text[:500] if resp.text else 'empty'}")

        if resp.status_code == 401:
            raise SequencerAuthError("Apollo API key is invalid or expired")
        if resp.status_code == 429:
            raise SequencerRateLimitError("Apollo rate limit exceeded")
        if resp.status_code >= 400:
            try:
                error_detail = resp.json()
            except Exception:
                error_detail = resp.text
            if (
                resp.status_code == 404
                and self.use_bearer_auth
                and "emailer_campaigns" in endpoint
                and "add_contact_ids" in endpoint
            ):
                raise SequencerError(
                    "Apollo API error 404 for "
                    f"{method} {url}: {error_detail}. "
                    "OAuth access to sequencing may be restricted; confirm the "
                    "OAuth scopes/permissions for add-to-sequence."
                )
            raise SequencerError(
                f"Apollo API error {resp.status_code} for {method} {url}: {error_detail}"
            )

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

    def _prospect_to_apollo(self, prospect: Prospect) -> Dict:
        """Convert Prospect to Apollo contact format for sequence enrollment."""
        contact = {
            "email": prospect.email,
        }
        if prospect.first_name:
            contact["first_name"] = prospect.first_name
        if prospect.last_name:
            contact["last_name"] = prospect.last_name
        if prospect.company:
            contact["organization_name"] = prospect.company
        if prospect.title:
            contact["title"] = prospect.title
        if prospect.phone:
            contact["phone"] = prospect.phone
        if prospect.linkedin_url:
            contact["linkedin_url"] = prospect.linkedin_url
        if prospect.domain:
            contact["website_url"] = prospect.domain

        # Custom variables for Apollo
        if prospect.custom_vars:
            contact["custom_fields"] = prospect.custom_vars

        return contact

    def _find_contact_id(self, email: str) -> Optional[str]:
        """Look up an Apollo contact by email and return the contact ID if found."""
        resp = self._request(
            "POST",
            "/api/v1/contacts/search",
            json={
                "q_keywords": email,
                "page": 1,
                "per_page": 1,
            },
        )
        contacts = resp.get("contacts", [])
        if not contacts:
            return None
        return contacts[0].get("id")

    def _create_contact(self, prospect: Prospect) -> Optional[str]:
        """Create an Apollo contact and return the new contact ID."""
        contact_payload = self._prospect_to_apollo(prospect)
        resp = self._request(
            "POST",
            "/api/v1/contacts",
            json=contact_payload,
        )
        return resp.get("contact", {}).get("id") or resp.get("id") or resp.get("contact_id")

    def upsert_prospects(
        self,
        prospects: List[Prospect],
        campaign_id: Optional[str] = None,
    ) -> BatchOutcome:
        """
        Enroll prospects in an Apollo email sequence.

        Apollo requires a sequence_id (campaign_id) for enrollment.
        Uses the /v1/emails/add_to_campaign endpoint.
        """
        import logging
        logger = logging.getLogger(__name__)
        logger.info(
            "[APOLLO-UPSERT] upsert_prospects called: campaign_id=%s prospect_count=%d use_bearer=%s",
            campaign_id,
            len(prospects) if prospects else 0,
            self.use_bearer_auth,
        )

        outcome = BatchOutcome()

        if not prospects:
            logger.info("[APOLLO-UPSERT] No prospects provided, returning empty outcome")
            return outcome

        if not campaign_id:
            raise SequencerError("Apollo sequencer requires a campaign_id (sequence_id)")
        default_send_from = self.send_email_from_email_account_id

        # Apollo enrolls one contact at a time via add_to_campaign
        # We batch requests but each is a separate API call
        for prospect in prospects:
            try:
                # Apollo docs: POST /api/v1/emailer_campaigns/{sequence_id}/add_contact_ids
                # contact_ids[] is required and must reference existing contacts.
                contact_id = self._find_contact_id(prospect.email)
                if not contact_id:
                    contact_id = self._create_contact(prospect)
                if not contact_id:
                    raise SequencerError("Apollo contact_id not found after create/search")

                send_from_id = (
                    getattr(prospect, "send_email_from_email_account_id", None)
                    or default_send_from
                )
                if not send_from_id:
                    raise SequencerError(
                        "Apollo requires send_email_from_email_account_id for sequence enrollment"
                    )

                add_params = {
                    "contact_ids[]": [contact_id],
                    "emailer_campaign_id": campaign_id,
                    "send_email_from_email_account_id": send_from_id,
                    "sequence_active_in_other_campaigns": False,
                }

                logger.info(
                    "[APOLLO-ENROLL] Enrolling email=%s in campaign=%s contact_id=%s params=%s",
                    prospect.email,
                    campaign_id,
                    contact_id,
                    add_params,
                )

                resp = self._request(
                    "POST",
                    f"/api/v1/emailer_campaigns/{campaign_id}/add_contact_ids",
                    params=add_params,
                )
                outcome.last_status_code = 200

                # Check response for success
                response_contact_id = resp.get("contact", {}).get("id") or resp.get("contact_id")
                provider_contact_id = response_contact_id or contact_id

                if resp.get("error") or resp.get("message", "").lower().startswith("error"):
                    outcome.failed.append(EnrollmentResult(
                        email=prospect.email,
                        campaign_id=campaign_id,
                        status="failed",
                        error=resp.get("error") or resp.get("message"),
                    ))
                elif "already" in str(resp.get("message", "")).lower():
                    # Already enrolled - treat as skip
                    outcome.skipped.append(EnrollmentResult(
                        email=prospect.email,
                        campaign_id=campaign_id,
                        status="exists",
                        provider_prospect_id=provider_contact_id,
                    ))
                else:
                    outcome.succeeded.append(EnrollmentResult(
                        email=prospect.email,
                        campaign_id=campaign_id,
                        status="enrolled",
                        provider_prospect_id=provider_contact_id,
                    ))

            except SequencerRateLimitError:
                outcome.last_status_code = 429
                outcome.failed.append(EnrollmentResult(
                    email=prospect.email,
                    campaign_id=campaign_id,
                    status="failed",
                    error="Rate limit exceeded",
                ))
                raise

            except SequencerError as e:
                outcome.failed.append(EnrollmentResult(
                    email=prospect.email,
                    campaign_id=campaign_id,
                    status="failed",
                    error=str(e),
                ))

        return outcome

    def enroll_in_campaign(
        self,
        prospect_ids: List[str],
        campaign_id: str,
    ) -> BatchOutcome:
        """Enroll existing Apollo contacts in a sequence by their contact IDs."""
        outcome = BatchOutcome()
        if not self.send_email_from_email_account_id:
            raise SequencerError(
                "Apollo requires send_email_from_email_account_id for sequence enrollment"
            )

        try:
            # Apollo bulk enrollment by contact IDs
            # Docs: POST /api/v1/emailer_campaigns/{sequence_id}/add_contact_ids
            params = {
                "contact_ids[]": prospect_ids,
                "emailer_campaign_id": campaign_id,
                "send_email_from_email_account_id": self.send_email_from_email_account_id,
            }
            resp = self._request(
                "POST",
                f"/api/v1/emailer_campaigns/{campaign_id}/add_contact_ids",
                params=params,
            )
            outcome.last_status_code = 200

            # Process results
            contacts = resp.get("contacts", [])
            for i, contact_id in enumerate(prospect_ids):
                if i < len(contacts):
                    outcome.succeeded.append(EnrollmentResult(
                        email="",  # We don't have email from just IDs
                        campaign_id=campaign_id,
                        status="enrolled",
                        provider_prospect_id=contact_id,
                    ))
                else:
                    outcome.succeeded.append(EnrollmentResult(
                        email="",
                        campaign_id=campaign_id,
                        status="enrolled",
                        provider_prospect_id=contact_id,
                    ))

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

        return outcome

    def list_campaigns(self) -> List[Campaign]:
        """List all Apollo email sequences (campaigns)."""
        campaigns = []
        page = 1
        per_page = 100

        while True:
            # OAuth uses POST /search endpoint (emailer_campaigns_search scope)
            # API keys use GET /api/v1/emailer_campaigns
            if self.use_bearer_auth:
                resp = self._request(
                    "POST",
                    "/api/v1/emailer_campaigns/search",
                    json={"page": page, "per_page": per_page},
                )
            else:
                resp = self._request(
                    "GET",
                    "/api/v1/emailer_campaigns",
                    params={"page": page, "per_page": per_page},
                )
            items = resp.get("emailer_campaigns", [])

            if not items:
                break

            for item in items:
                campaigns.append(Campaign(
                    id=str(item.get("id", "")),
                    name=item.get("name", ""),
                    status=item.get("active") and "active" or "inactive",
                    metadata={
                        "created_at": item.get("created_at"),
                        "num_steps": item.get("num_steps"),
                        "active": item.get("active"),
                        "archived": item.get("archived"),
                    },
                ))

            # Check pagination
            pagination = resp.get("pagination", {})
            if page >= pagination.get("total_pages", 1):
                break
            page += 1

        return campaigns

    def create_campaign(self, name: str, **kwargs) -> Campaign:
        """
        Create a new Apollo campaign.

        Apollo does not support campaign creation via API.
        Campaigns must be created manually in the Apollo dashboard.

        Raises:
            SequencerError: Always, as this operation is not supported
        """
        raise SequencerError(
            "Apollo does not support campaign creation via API. "
            "Create campaigns in the Apollo dashboard at https://app.apollo.io"
        )

    def list_email_accounts(self) -> List[Dict[str, Any]]:
        """List Apollo email accounts (senders)."""
        resp = self._request(
            "GET",
            "/api/v1/email_accounts",
        )
        if isinstance(resp, list):
            return resp
        if not isinstance(resp, dict):
            return []
        return (
            resp.get("email_accounts")
            or resp.get("accounts")
            or resp.get("data")
            or []
        )

    def verify_webhook(self, request: WebhookRequest) -> bool:
        """
        Apollo webhook verification.

        Apollo does not document webhook signatures.
        Recommend network-level security (IP allowlisting, Cloudflare Access).
        """
        # Apollo has no documented signature verification
        return True

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

        # Parse timestamp
        timestamp_str = raw_event.get("timestamp", raw_event.get("created_at", ""))
        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="apollo",
            provider_event_id=raw_event.get("id", ""),
            email=raw_event.get("email", raw_event.get("contact_email", "")),
            campaign_id=str(raw_event.get("emailer_campaign_id", raw_event.get("sequence_id", ""))),
            campaign_name=raw_event.get("campaign_name", raw_event.get("sequence_name")),
            raw_payload=raw_event,
        )

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

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

        return event

    def test_connection(self) -> Dict[str, Any]:
        """Test API connectivity and return account info."""
        sequences_available = False
        enrichment_available = False

        # Try sequences API first (requires higher tier plan)
        try:
            campaigns = self.list_campaigns()
            sequences_available = True
            return {
                "status": "ok",
                "provider": "apollo",
                "campaign_count": len(campaigns),
                "sequences_available": True,
                "enrichment_available": True,  # If sequences work, enrichment works
            }
        except SequencerAuthError:
            return {
                "status": "error",
                "provider": "apollo",
                "error": "Authentication failed - check API key",
            }
        except SequencerError as seq_error:
            # Sequences API not available - try enrichment endpoint
            # This is common for Basic plan users
            try:
                import requests
                headers = {"Content-Type": "application/json", "x-api-key": self.api_key}
                resp = requests.post(
                    "https://api.apollo.io/v1/people/match",
                    json={"email": "test@example.com"},
                    headers=headers,
                    timeout=10,
                )
                # 200 or 422 (no results) means API key works
                if resp.status_code in (200, 422):
                    enrichment_available = True
            except Exception:
                pass

            if enrichment_available:
                return {
                    "status": "partial",
                    "provider": "apollo",
                    "sequences_available": False,
                    "enrichment_available": True,
                    "note": "Sequences API not available (requires Organization/Enterprise plan). Enrichment API works.",
                }
            else:
                return {
                    "status": "error",
                    "provider": "apollo",
                    "error": str(seq_error),
                }

    def get_contact_sequence_status(
        self,
        email: str,
        campaign_id: Optional[str] = None,
    ) -> Dict[str, Any]:
        """
        Check if a contact is enrolled in a sequence.

        Useful for deduplication before enrollment.
        """
        try:
            resp = self._request(
                "POST",
                "/api/v1/contacts/search",
                json={
                    "q_keywords": email,
                    "page": 1,
                    "per_page": 1,
                },
            )
            contacts = resp.get("contacts", [])
            if not contacts:
                return {"enrolled": False, "contact_id": None}

            contact = contacts[0]
            contact_id = contact.get("id")

            # Check sequence enrollment
            enrollments = contact.get("emailer_campaign_ids", [])
            if campaign_id:
                enrolled = campaign_id in [str(e) for e in enrollments]
            else:
                enrolled = len(enrollments) > 0

            return {
                "enrolled": enrolled,
                "contact_id": contact_id,
                "active_sequences": enrollments,
            }

        except SequencerError:
            return {"enrolled": False, "contact_id": None, "error": "Lookup failed"}

    def stop_sequence(self, email: str, campaign_id: str) -> bool:
        """Remove a contact from a sequence."""
        try:
            # First find the contact
            status = self.get_contact_sequence_status(email, campaign_id)
            if not status.get("contact_id"):
                return False

            # Remove from sequence
            self._request(
                "POST",
                "/v1/emailer_campaigns/remove_contact_ids",
                json={
                    "emailer_campaign_id": campaign_id,
                    "contact_ids": [status["contact_id"]],
                },
            )
            return True

        except SequencerError:
            return False

    def mark_as_replied(self, email: str, campaign_id: str) -> bool:
        """Mark a contact as replied to stop sequence automation."""
        try:
            status = self.get_contact_sequence_status(email, campaign_id)
            if not status.get("contact_id"):
                return False

            self._request(
                "POST",
                f"/v1/emailer_campaigns/{campaign_id}/mark_contact_as_replied",
                json={
                    "contact_id": status["contact_id"],
                },
            )
            return True

        except SequencerError:
            return False
