"""
Outreach sequencer provider implementation.

Supports Outreach sequences and mailboxes via OAuth access tokens.
"""

from __future__ import annotations

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

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


OUTREACH_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",
    "meeting_booked": "meeting_booked",
    "out_of_office": "out_of_office",
}


class OutreachSequencerProvider(BaseProvider):
    """Outreach sequencer provider using OAuth Bearer authentication."""

    BASE_URL = "https://api.outreach.io/api/v2"

    def __init__(
        self,
        api_key: str,
        timeout: int = 30,
        base_url: Optional[str] = None,
        use_bearer_auth: bool = True,  # Accepted for interface consistency.
        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.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 "outreach"

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

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

    def _request(
        self,
        method: str,
        endpoint: str,
        json: Optional[Dict[str, Any]] = None,
        params: Optional[Dict[str, Any]] = None,
        allow_statuses: tuple[int, ...] = (),
    ):
        session = self._get_session()
        if endpoint.startswith("http://") or endpoint.startswith("https://"):
            url = endpoint
        else:
            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 in allow_statuses:
            return resp
        if resp.status_code in {401, 403}:
            raise SequencerAuthError(self._error_message(resp, "Outreach authentication failed"))
        if resp.status_code == 429:
            retry_after = self._parse_retry_after(resp.headers.get("Retry-After"))
            raise SequencerRateLimitError("Outreach rate limit exceeded", retry_after=retry_after)
        if resp.status_code >= 400:
            raise SequencerError(
                f"Outreach API error {resp.status_code}: {self._error_message(resp)}"
            )
        return resp

    @staticmethod
    def _parse_retry_after(value: Optional[str]) -> Optional[float]:
        if not value:
            return None
        try:
            return float(value)
        except (TypeError, ValueError):
            return None

    @staticmethod
    def _response_json(resp) -> Dict[str, Any]:
        try:
            payload = resp.json()
        except Exception:
            return {}
        return payload if isinstance(payload, dict) else {}

    @classmethod
    def _error_message(cls, resp, fallback: Optional[str] = None) -> str:
        payload = cls._response_json(resp)
        errors = payload.get("errors")
        if isinstance(errors, list) and errors:
            details: List[str] = []
            for item in errors:
                if not isinstance(item, dict):
                    continue
                detail = (
                    item.get("detail")
                    or item.get("title")
                    or item.get("message")
                    or item.get("status")
                )
                if detail:
                    text = str(detail)
                    if text not in details:
                        details.append(text)
            if details:
                return "; ".join(details)
        text = str(getattr(resp, "text", "") or "").strip()
        if text:
            return text
        return fallback or f"HTTP {getattr(resp, 'status_code', 'error')}"

    @staticmethod
    def _mailbox_email(attrs: Dict[str, Any]) -> Optional[str]:
        for key in ("email", "mailboxAddress", "fromEmail", "address", "emailAddress"):
            value = attrs.get(key)
            if value:
                return str(value)
        return None

    def _prospect_to_outreach(self, prospect: Prospect) -> OutreachProspect:
        custom_vars = {
            key: value
            for key, value in (prospect.custom_vars or {}).items()
            if value is not None
        }
        return OutreachProspect(
            email=prospect.email,
            first_name=prospect.first_name,
            last_name=prospect.last_name,
            title=prospect.title,
            company_name=prospect.company,
            linkedin_url=prospect.linkedin_url,
            phone=prospect.phone,
            custom=custom_vars,
        )

    def _resolve_mailbox_id(self, prospect: Optional[Prospect] = None) -> Optional[str]:
        if prospect and prospect.send_email_from_email_account_id:
            return prospect.send_email_from_email_account_id
        return self.send_email_from_email_account_id

    def _find_prospect_id_by_email(self, email: str) -> Optional[str]:
        resp = self._request("GET", "/prospects", params={"filter[emails]": email})
        payload = self._response_json(resp)
        items = payload.get("data") or []
        if not items:
            return None
        prospect = items[0] if isinstance(items[0], dict) else {}
        prospect_id = prospect.get("id")
        return str(prospect_id) if prospect_id is not None else None

    def _find_prospect_id_by_email_address(self, email: str) -> Optional[str]:
        resp = self._request(
            "GET",
            "/emailAddresses",
            params={"filter[email]": email, "page[size]": 1, "count": "false"},
        )
        payload = self._response_json(resp)
        items = payload.get("data") or []
        if not items:
            return None
        email_address = items[0] if isinstance(items[0], dict) else {}
        relationships = email_address.get("relationships") or {}
        prospect_rel = relationships.get("prospect") or {}
        prospect_data = prospect_rel.get("data") or {}
        prospect_id = prospect_data.get("id")
        return str(prospect_id) if prospect_id is not None else None

    def _find_user_id_by_email(self, email: str) -> Optional[str]:
        resp = self._request(
            "GET",
            "/users",
            params={"filter[email]": email, "page[size]": 1, "count": "false"},
        )
        payload = self._response_json(resp)
        items = payload.get("data") or []
        if not items:
            return None
        user = items[0] if isinstance(items[0], dict) else {}
        user_id = user.get("id")
        return str(user_id) if user_id is not None else None

    @classmethod
    def _response_error_codes(cls, resp) -> set[str]:
        payload = cls._response_json(resp)
        errors = payload.get("errors")
        codes: set[str] = set()
        if not isinstance(errors, list):
            return codes
        for item in errors:
            if not isinstance(item, dict):
                continue
            code = item.get("code") or item.get("id")
            if code:
                codes.add(str(code))
        return codes

    def _create_prospect(self, prospect: Prospect) -> str:
        payload = self._prospect_to_outreach(prospect).to_api_format()
        resp = self._request("POST", "/prospects", json=payload, allow_statuses=(422,))
        if resp.status_code == 422:
            error_codes = self._response_error_codes(resp)
            if "is_using_excluded_email_address" in error_codes:
                if self._find_user_id_by_email(prospect.email):
                    raise SequencerError(
                        "Outreach cannot create or sequence this email because it belongs to an Outreach user/mailbox in this workspace."
                    )
                raise SequencerError(
                    "Outreach cannot create or sequence this email because it is an excluded email address in this workspace."
                )
            raise SequencerError(
                f"Outreach API error 422: {self._error_message(resp, 'Outreach validation error')}"
            )
        body = self._response_json(resp)
        data = body.get("data") or {}
        prospect_id = data.get("id")
        if prospect_id is None:
            raise SequencerError("Outreach prospect creation returned no id")
        return str(prospect_id)

    @staticmethod
    def _is_duplicate_email_error(error: Any) -> bool:
        message = str(error or "").lower()
        return "email has already been taken" in message

    def _find_existing_prospect_id(self, email: str) -> Optional[str]:
        prospect_id = self._find_prospect_id_by_email(email)
        if prospect_id:
            return prospect_id
        return self._find_prospect_id_by_email_address(email)

    def _find_or_create_prospect_id(self, prospect: Prospect) -> str:
        prospect_id = self._find_existing_prospect_id(prospect.email)
        if prospect_id:
            return prospect_id
        try:
            return self._create_prospect(prospect)
        except SequencerError as exc:
            if not self._is_duplicate_email_error(exc):
                raise
            prospect_id = self._find_existing_prospect_id(prospect.email)
            if prospect_id:
                return prospect_id
            raise

    def _add_to_sequence(
        self,
        prospect_id: str,
        campaign_id: str,
        mailbox_id: Optional[str] = None,
    ) -> Dict[str, Any]:
        payload: Dict[str, Any] = {
            "data": {
                "type": "sequenceState",
                "relationships": {
                    "prospect": {"data": {"type": "prospect", "id": prospect_id}},
                    "sequence": {"data": {"type": "sequence", "id": campaign_id}},
                },
            }
        }
        if mailbox_id:
            payload["data"]["relationships"]["mailbox"] = {
                "data": {"type": "mailbox", "id": mailbox_id}
            }

        resp = self._request(
            "POST",
            "/sequenceStates",
            json=payload,
            allow_statuses=(409, 422),
        )
        body = self._response_json(resp)

        if resp.status_code == 409:
            return {"status": "already_enrolled"}
        if resp.status_code == 422:
            return {
                "status": "failed",
                "error": self._error_message(resp, "Outreach validation error"),
            }

        data = body.get("data") or {}
        return {
            "status": "enrolled",
            "sequence_state_id": str(data.get("id")) if data.get("id") is not None else None,
        }

    def upsert_prospects(
        self,
        prospects: List[Prospect],
        campaign_id: Optional[str] = None,
    ) -> BatchOutcome:
        outcome = BatchOutcome()

        if not prospects:
            return outcome
        if not campaign_id:
            raise SequencerError("Outreach requires a campaign_id (sequence_id)")

        for prospect in prospects:
            try:
                prospect_id = self._find_or_create_prospect_id(prospect)

                enrollment = self._add_to_sequence(
                    prospect_id,
                    campaign_id,
                    mailbox_id=self._resolve_mailbox_id(prospect),
                )
                outcome.last_status_code = 200

                if enrollment["status"] == "enrolled":
                    outcome.succeeded.append(
                        EnrollmentResult(
                            email=prospect.email,
                            campaign_id=campaign_id,
                            status="enrolled",
                            provider_prospect_id=prospect_id,
                        )
                    )
                elif enrollment["status"] == "already_enrolled":
                    outcome.skipped.append(
                        EnrollmentResult(
                            email=prospect.email,
                            campaign_id=campaign_id,
                            status="exists",
                            provider_prospect_id=prospect_id,
                        )
                    )
                else:
                    outcome.failed.append(
                        EnrollmentResult(
                            email=prospect.email,
                            campaign_id=campaign_id,
                            status="failed",
                            provider_prospect_id=prospect_id,
                            error=enrollment.get("error"),
                        )
                    )
            except (SequencerAuthError, SequencerRateLimitError):
                raise
            except SequencerError as exc:
                outcome.failed.append(
                    EnrollmentResult(
                        email=prospect.email,
                        campaign_id=campaign_id,
                        status="failed",
                        error=str(exc),
                    )
                )

        return outcome

    def enroll_in_campaign(
        self,
        prospect_ids: List[str],
        campaign_id: str,
    ) -> BatchOutcome:
        outcome = BatchOutcome()
        if not campaign_id:
            raise SequencerError("Outreach requires a campaign_id (sequence_id)")

        for prospect_id in prospect_ids:
            try:
                enrollment = self._add_to_sequence(
                    prospect_id,
                    campaign_id,
                    mailbox_id=self.send_email_from_email_account_id,
                )
                outcome.last_status_code = 200
                if enrollment["status"] == "enrolled":
                    outcome.succeeded.append(
                        EnrollmentResult(
                            email="",
                            campaign_id=campaign_id,
                            status="enrolled",
                            provider_prospect_id=prospect_id,
                        )
                    )
                elif enrollment["status"] == "already_enrolled":
                    outcome.skipped.append(
                        EnrollmentResult(
                            email="",
                            campaign_id=campaign_id,
                            status="exists",
                            provider_prospect_id=prospect_id,
                        )
                    )
                else:
                    outcome.failed.append(
                        EnrollmentResult(
                            email="",
                            campaign_id=campaign_id,
                            status="failed",
                            provider_prospect_id=prospect_id,
                            error=enrollment.get("error"),
                        )
                    )
            except (SequencerAuthError, SequencerRateLimitError):
                raise
            except SequencerError as exc:
                outcome.failed.append(
                    EnrollmentResult(
                        email="",
                        campaign_id=campaign_id,
                        status="failed",
                        provider_prospect_id=prospect_id,
                        error=str(exc),
                    )
                )
        return outcome

    def list_campaigns(self) -> List[Campaign]:
        campaigns: List[Campaign] = []
        endpoint = "/sequences"
        params: Dict[str, Any] = {"page[size]": 100}

        while endpoint and len(campaigns) < 1000:
            resp = self._request("GET", endpoint, params=params)
            payload = self._response_json(resp)

            for item in payload.get("data") or []:
                if not isinstance(item, dict):
                    continue
                attrs = item.get("attributes") or {}
                enabled = attrs.get("enabled")
                if enabled is False:
                    continue
                campaigns.append(
                    Campaign(
                        id=str(item.get("id") or ""),
                        name=str(attrs.get("name") or ""),
                        status="active",
                        metadata={
                            "enabled": enabled,
                            "sequence_type": attrs.get("sequenceType"),
                            "step_count": attrs.get("stepCount"),
                        },
                    )
                )

            links = payload.get("links") or {}
            next_link = links.get("next") if isinstance(links, dict) else None
            endpoint = str(next_link) if next_link else ""
            params = {}

        return campaigns

    def create_campaign(self, name: str, **kwargs) -> Campaign:
        raise SequencerError(
            "Outreach does not support sequence creation via API. "
            "Create sequences in the Outreach dashboard."
        )

    def list_email_accounts(self) -> List[Dict[str, Any]]:
        accounts: List[Dict[str, Any]] = []
        endpoint = "/mailboxes"
        params: Dict[str, Any] = {"page[size]": 100}

        while endpoint and len(accounts) < 1000:
            resp = self._request("GET", endpoint, params=params)
            payload = self._response_json(resp)
            included = payload.get("included") or []
            included_users: Dict[str, Dict[str, Any]] = {}
            for item in included:
                if not isinstance(item, dict):
                    continue
                if item.get("type") != "user":
                    continue
                user_id = item.get("id")
                if user_id is None:
                    continue
                included_users[str(user_id)] = item.get("attributes") or {}

            for item in payload.get("data") or []:
                if not isinstance(item, dict):
                    continue
                attrs = item.get("attributes") or {}
                relationships = item.get("relationships") or {}
                user_data = (
                    (((relationships.get("user") or {}).get("data") or {}).get("id"))
                    if isinstance(relationships, dict)
                    else None
                )
                user_attrs = included_users.get(str(user_data)) if user_data is not None else None
                accounts.append(
                    {
                        "id": str(item.get("id") or ""),
                        "email": self._mailbox_email(attrs),
                        "name": attrs.get("name") or attrs.get("displayName"),
                        "status": attrs.get("state") or attrs.get("status"),
                        "user_id": str(user_data) if user_data is not None else None,
                        "user_email": (user_attrs or {}).get("email"),
                    }
                )

            links = payload.get("links") or {}
            next_link = links.get("next") if isinstance(links, dict) else None
            endpoint = str(next_link) if next_link else ""
            params = {}

        return accounts

    def verify_webhook(self, request: WebhookRequest) -> bool:
        raise NotImplementedError(
            "Outreach webhook verification is not implemented. "
            "Add signature validation before enabling Outreach webhook ingestion."
        )

    def normalize_event(self, raw_event: Dict[str, Any]) -> NormalizedEvent:
        event_type_raw = str(raw_event.get("event_type") or raw_event.get("type") or "").lower()
        event_type = OUTREACH_EVENT_MAP.get(event_type_raw, "category_updated")
        timestamp_raw = raw_event.get("timestamp") or raw_event.get("created_at")
        if isinstance(timestamp_raw, str):
            try:
                timestamp = datetime.fromisoformat(timestamp_raw.replace("Z", "+00:00"))
            except ValueError:
                timestamp = datetime.now(timezone.utc)
        else:
            timestamp = datetime.now(timezone.utc)

        event = NormalizedEvent(
            event_type=event_type,
            timestamp=timestamp,
            provider="outreach",
            provider_event_id=str(raw_event.get("id") or ""),
            email=str(raw_event.get("email") or ""),
            campaign_id=str(raw_event.get("sequence_id") or raw_event.get("campaign_id") or ""),
            campaign_name=raw_event.get("sequence_name") or raw_event.get("campaign_name"),
            raw_payload=raw_event,
        )
        if event_type in {"replied", "auto_replied"}:
            event.reply_text = raw_event.get("reply_text") or raw_event.get("body")
            event.reply_subject = raw_event.get("reply_subject") or raw_event.get("subject")
        if event_type == "category_updated":
            event.category = str(raw_event.get("category") or raw_event.get("label") or "")
        return event

    def test_connection(self) -> Dict[str, Any]:
        campaigns = self.list_campaigns()
        return {
            "status": "ok",
            "provider": "outreach",
            "campaign_count": len(campaigns),
            "sequences_available": True,
        }
