"""
Amplemarket sales engagement client.

Supports:
- Identity checks
- Sequence discovery
- Contact lookup/listing
- Sequence enrollment (bulk)
"""

from __future__ import annotations

import csv
import logging
import re
import time
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any, Dict, Iterable, List, Optional
from urllib.parse import parse_qs, quote, urlparse

import click

from g_gremlin.base import (
    AuthenticationError,
    BaseIntegration,
    IntegrationError,
    RateLimitError,
    RetryConfig,
    TransientError,
)
from g_gremlin.contracts import BatchOutcome, CapabilityCheck
from g_gremlin.credentials import resolve_credentials
from g_gremlin.credentials.provider import CredentialProvider, SecretNotFoundError
from g_gremlin.pacing import AmplemarketPacingProfile, get_amplemarket_profile

logger = logging.getLogger(__name__)

API_BASE = "https://api.amplemarket.com"
DEFAULT_RETRY_CONFIG = RetryConfig(max_attempts=4, base_delay=1.2, max_delay=15.0)
ERROR_STATUSES = (400, 402, 409, 422)
CONTACT_FIELDS = {
    "email",
    "first_name",
    "last_name",
    "title",
    "company_name",
    "linkedin_url",
    "phone",
}

RATE_LIMITS = {
    "people_search": 300,  # per minute
    "people_find": 350,  # per minute
    "sequence_leads": 30,  # per minute
    "default": 500,  # per minute
}

LEAD_LIST_BATCH_MAX = 10000
LEAD_LIST_MAX = 60000
LEAD_LIST_TYPES = {"linkedin", "email", "titles_and_company"}
LEAD_LIST_TYPE_ALIASES = {
    "title_and_company": "titles_and_company",
}

SEQUENCE_DATA_FIELD_RE = re.compile(r"^[a-z][a-z0-9_]{0,254}$")
SEQUENCE_DATA_FIELDS_MAX = 50
SEQUENCE_DATA_VALUE_MAX = 1024
SEQUENCE_LEADS_MAX = 250


def _prune_payload(value: Any) -> Any:
    if isinstance(value, dict):
        cleaned: Dict[str, Any] = {}
        for key, item in value.items():
            pruned = _prune_payload(item)
            if pruned is None:
                continue
            if isinstance(pruned, (dict, list)) and not pruned:
                continue
            if pruned == "":
                continue
            cleaned[key] = pruned
        return cleaned
    if isinstance(value, list):
        cleaned_list = []
        for item in value:
            pruned = _prune_payload(item)
            if pruned is None:
                continue
            if isinstance(pruned, (dict, list)) and not pruned:
                continue
            if pruned == "":
                continue
            cleaned_list.append(pruned)
        return cleaned_list
    return value


class _RateLimiter:
    def __init__(self, rate_per_minute: float) -> None:
        self.capacity = max(rate_per_minute, 0.0)
        self.tokens = self.capacity
        self.refill_rate = self.capacity / 60.0 if self.capacity else 0.0
        self.last_check = time.monotonic()

    def acquire(self, sleep_fn, tokens: float = 1.0) -> None:
        if not self.refill_rate:
            return
        now = time.monotonic()
        elapsed = max(now - self.last_check, 0.0)
        self.tokens = min(self.capacity, self.tokens + elapsed * self.refill_rate)
        self.last_check = now
        if self.tokens >= tokens:
            self.tokens -= tokens
            return
        needed = tokens - self.tokens
        wait_for = needed / self.refill_rate if self.refill_rate else 0.0
        if wait_for > 0:
            sleep_fn(wait_for)
        self.last_check = time.monotonic()
        self.tokens = max(self.tokens - tokens, 0.0)


class AmplemarketError(RuntimeError):
    """Base Amplemarket client error."""


class AmplemarketAuthError(AmplemarketError, AuthenticationError):
    """Authentication failed."""

    def __init__(self, message: str):
        AuthenticationError.__init__(self, message)


class AmplemarketRateLimitError(AmplemarketError, RateLimitError):
    """Raised when Amplemarket rate limits requests."""

    def __init__(self, message: str, retry_after: float | None = None):
        RateLimitError.__init__(self, message, retry_after=retry_after)


class InsufficientCreditsError(AmplemarketError):
    """Raised when the API reports insufficient credits."""


class MissingFieldError(AmplemarketError):
    """Raised when a required field is missing."""


class UnsupportedValueError(AmplemarketError):
    """Raised when an unsupported value is provided."""


@dataclass
class AmplemarketContact:
    email: Optional[str] = None
    first_name: Optional[str] = None
    last_name: Optional[str] = None
    title: Optional[str] = None
    company_name: Optional[str] = None
    linkedin_url: Optional[str] = None
    phone: Optional[str] = None

    def to_api_format(self) -> Dict[str, Any]:
        payload: Dict[str, Any] = {}
        if self.email:
            payload["email"] = self.email
        if self.first_name:
            payload["first_name"] = self.first_name
        if self.last_name:
            payload["last_name"] = self.last_name
        if self.title:
            payload["title"] = self.title
        if self.company_name:
            payload["company_name"] = self.company_name
        if self.linkedin_url:
            payload["linkedin_url"] = self.linkedin_url
        if self.phone:
            payload["phone"] = self.phone
        return payload


@dataclass
class AmplemarketSequence:
    id: str
    name: str
    status: str = ""
    step_count: int = 0


@dataclass
class AmplemarketLeadList:
    id: str
    name: str
    status: str = ""
    lead_type: str = ""
    shared: Optional[bool] = None
    visible: Optional[bool] = None
    owner_email: Optional[str] = None
    total_leads: Optional[int] = None
    raw: Dict[str, Any] = field(default_factory=dict)


@dataclass
class AmplemarketEnrichmentRequest:
    id: str
    status: str = ""
    request_type: str = ""
    total_requested: Optional[int] = None
    total_enriched: Optional[int] = None
    raw: Dict[str, Any] = field(default_factory=dict)


@dataclass
class AmplemarketSearchPage:
    results: List[Dict[str, Any]]
    pagination: Dict[str, Any] = field(default_factory=dict)
    raw: Dict[str, Any] = field(default_factory=dict)


@dataclass
class AmplemarketSequenceLeadPayload:
    email: Optional[str] = None
    linkedin_url: Optional[str] = None
    data: Dict[str, Any] = field(default_factory=dict)
    overrides: Dict[str, Any] = field(default_factory=dict)

    def to_api_format(self) -> Dict[str, Any]:
        payload: Dict[str, Any] = {}
        if self.email:
            payload["email"] = self.email
        if self.linkedin_url:
            payload["linkedin_url"] = self.linkedin_url
        if self.data:
            payload["data"] = self.data
        if self.overrides:
            payload["overrides"] = self.overrides
        return payload


@dataclass
class EnrollmentResult:
    total: int
    total_added: int
    duplicate_emails: List[str]
    duplicate_linkedin_urls: List[str]
    already_in_sequence: List[str]
    excluded: List[str]
    added_ids_by_email: Dict[str, str] = field(default_factory=dict)
    added_ids_by_linkedin: Dict[str, str] = field(default_factory=dict)

    def provider_id_for(
        self,
        email: Optional[str] = None,
        linkedin_url: Optional[str] = None,
    ) -> Optional[str]:
        email_key = (email or "").strip().lower()
        if email_key:
            provider_id = self.added_ids_by_email.get(email_key)
            if provider_id:
                return provider_id
        linkedin_key = (linkedin_url or "").strip().lower()
        if linkedin_key:
            provider_id = self.added_ids_by_linkedin.get(linkedin_key)
            if provider_id:
                return provider_id
        return None

    def to_batch_outcome(self) -> BatchOutcome:
        outcome = BatchOutcome()
        for _ in range(max(self.total_added, 0)):
            outcome.succeeded.append({"status": "added"})
        for email in self.excluded:
            outcome.failed.append({"email": email, "status": "excluded"})
        for email in self.duplicate_emails:
            outcome.retryable_failed.append({"email": email, "status": "duplicate"})
        for url in self.duplicate_linkedin_urls:
            outcome.retryable_failed.append({"linkedin_url": url, "status": "duplicate"})
        return outcome


def get_amplemarket_api_key(credential_provider: CredentialProvider | None = None) -> str:
    provider = credential_provider or CredentialProvider()
    try:
        creds = resolve_credentials("amplemarket", required_keys=["api_key"], credential_provider=provider)
        return creds["api_key"]
    except SecretNotFoundError as exc:
        raise click.ClickException(
            "Amplemarket not configured. Set up with:\n"
            "  g-gremlin auth set amplemarket                 # Store in keychain\n"
            "  export G_GREMLIN_AMPLEMARKET_API_KEY=your-key  # Or use env var"
        ) from exc


class AmplemarketClient(BaseIntegration):
    SERVICE_NAME = "amplemarket"

    def __init__(
        self,
        api_key: Optional[str] = None,
        credential_provider: CredentialProvider | None = None,
        retry_config: RetryConfig | None = None,
        api_base: Optional[str] = None,
    ) -> None:
        super().__init__(credential_provider=credential_provider, default_retry_config=retry_config or DEFAULT_RETRY_CONFIG)
        self.api_key = api_key or get_amplemarket_api_key(credential_provider)
        self.api_base = api_base or API_BASE
        self._rate_limiters: Dict[str, _RateLimiter] = {}

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

    def _normalize_path(self, path: str) -> str:
        parsed = urlparse(path) if path.startswith("http") else urlparse(path)
        return parsed.path or path.split("?")[0]

    def _rate_limit_key(self, path: str) -> str:
        normalized = self._normalize_path(path)
        if normalized.startswith("/people/search"):
            return "people_search"
        if normalized.startswith("/people/find"):
            return "people_find"
        if "/sequences/" in normalized and normalized.endswith("/leads"):
            return "sequence_leads"
        return "default"

    def _rate_limit(self, path: str) -> None:
        key = self._rate_limit_key(path)
        limit = RATE_LIMITS.get(key, RATE_LIMITS["default"])
        limiter = self._rate_limiters.get(key)
        if limiter is None:
            limiter = _RateLimiter(limit)
            self._rate_limiters[key] = limiter
        limiter.acquire(self._sleep)

    def _retry_after_seconds(self, response) -> float | None:
        retry_after = super()._retry_after_seconds(response)
        if retry_after is None:
            return None
        return retry_after + 1.0

    def _am_request(
        self,
        method: str,
        path: str,
        *,
        params: Optional[Dict[str, Any]] = None,
        json: Optional[Dict[str, Any]] = None,
        allow_statuses: tuple[int, ...] = (),
    ):
        url = path if path.startswith("http") else f"{self.api_base}{path}"
        merged_allow = tuple(set(allow_statuses).union(ERROR_STATUSES))
        self._rate_limit(path)
        try:
            resp = self._request(
                method,
                url,
                params=params,
                json=json,
                headers=self._headers(),
                allow_statuses=merged_allow,
            )
        except AuthenticationError as exc:
            raise AmplemarketAuthError(str(exc)) from exc
        except RateLimitError as exc:
            raise AmplemarketRateLimitError(str(exc), retry_after=exc.retry_after) from exc
        except (IntegrationError, TransientError) as exc:
            raise AmplemarketError(str(exc)) from exc

        if resp.ok or resp.status_code in allow_statuses:
            return resp

        self._raise_for_errors(resp)

    def _raise_for_errors(self, resp) -> None:
        detail = f"Amplemarket error {resp.status_code}"
        try:
            payload = resp.json()
        except Exception:
            raise AmplemarketError(f"{detail}: {resp.text[:200]}")

        code = ""
        message = ""
        if isinstance(payload, dict):
            errors = payload.get("_errors") or payload.get("errors")
            if isinstance(errors, list) and errors:
                err = errors[0] or {}
                code = str(err.get("code") or "").strip()
                message = err.get("detail") or err.get("message") or err.get("title") or ""

        if code == "insufficient_credits":
            raise InsufficientCreditsError(message or detail)
        if code == "missing_field":
            raise MissingFieldError(message or detail)
        if code == "unsupported_value":
            raise UnsupportedValueError(message or detail)
        if code:
            raise AmplemarketError(message or detail)

        if isinstance(payload, dict):
            message = payload.get("message") or payload.get("detail") or ""
        raise AmplemarketError(message or detail)

    def _parse_sequence(self, data: Dict[str, Any]) -> Optional[AmplemarketSequence]:
        if not isinstance(data, dict):
            return None
        sequence_id = data.get("id") or data.get("sequence_id") or data.get("sequenceId")
        if sequence_id is None:
            return None
        name = data.get("name") or data.get("sequence_name") or data.get("sequenceName") or ""
        status = data.get("status") or data.get("state") or ""
        step_count = data.get("step_count") or data.get("stepCount")
        if step_count is None:
            steps = data.get("steps")
            if isinstance(steps, list):
                step_count = len(steps)
        try:
            step_count_int = int(step_count or 0)
        except (TypeError, ValueError):
            step_count_int = 0

        return AmplemarketSequence(
            id=str(sequence_id),
            name=str(name),
            status=str(status),
            step_count=step_count_int,
        )

    def _parse_contact(self, data: Dict[str, Any]) -> Optional[AmplemarketContact]:
        if not isinstance(data, dict):
            return None
        return AmplemarketContact(
            email=data.get("email") or data.get("email_address") or data.get("emailAddress"),
            first_name=data.get("first_name") or data.get("firstName"),
            last_name=data.get("last_name") or data.get("lastName"),
            title=data.get("title"),
            company_name=data.get("company_name") or data.get("company") or data.get("companyName"),
            linkedin_url=data.get("linkedin_url") or data.get("linkedinUrl"),
            phone=data.get("phone") or data.get("phone_number") or data.get("phoneNumber"),
        )

    def _coerce_contact(self, lead: AmplemarketContact | Dict[str, Any]) -> AmplemarketContact:
        if isinstance(lead, AmplemarketContact):
            return lead
        if isinstance(lead, dict):
            filtered = {key: lead.get(key) for key in CONTACT_FIELDS if key in lead}
            return AmplemarketContact(**filtered)
        raise AmplemarketError("Invalid lead payload provided.")

    def _sanitize_sequence_data(self, data: Dict[str, Any]) -> Dict[str, str]:
        if not isinstance(data, dict):
            raise UnsupportedValueError("Sequence data must be a dictionary.")
        if len(data) > SEQUENCE_DATA_FIELDS_MAX:
            raise UnsupportedValueError("Sequence data supports up to 50 fields.")
        cleaned: Dict[str, str] = {}
        for key, value in data.items():
            if key is None:
                continue
            key_str = str(key).strip()
            if not key_str or not SEQUENCE_DATA_FIELD_RE.match(key_str):
                raise UnsupportedValueError(f"Invalid data field name: {key_str}")
            if value is None:
                continue
            if isinstance(value, (dict, list, tuple, set)):
                raise UnsupportedValueError(f"Invalid data field value for '{key_str}'")
            value_str = str(value)
            if len(value_str) > SEQUENCE_DATA_VALUE_MAX:
                raise UnsupportedValueError(f"Data field '{key_str}' exceeds 1024 characters.")
            cleaned[key_str] = value_str
        return cleaned

    def _coerce_sequence_lead(self, lead: Any) -> Dict[str, Any]:
        if isinstance(lead, AmplemarketSequenceLeadPayload):
            payload = lead.to_api_format()
        elif isinstance(lead, AmplemarketContact):
            payload = lead.to_api_format()
        elif isinstance(lead, dict):
            payload = {k: v for k, v in lead.items() if v is not None}
        else:
            raise AmplemarketError("Invalid lead payload provided.")

        if "data" in payload:
            payload["data"] = self._sanitize_sequence_data(payload.get("data") or {})

        if not payload.get("email") and not payload.get("linkedin_url"):
            raise MissingFieldError("Lead must include email or linkedin_url.")

        return payload

    def _extract_provider_id(self, item: Dict[str, Any]) -> Optional[str]:
        for key in (
            "id",
            "lead_id",
            "leadId",
            "person_id",
            "personId",
            "contact_id",
            "contactId",
        ):
            value = item.get(key)
            if value is None:
                continue
            text = str(value).strip()
            if text:
                return text
        return None

    def _extract_added_ids(
        self,
        payload: Dict[str, Any],
        data: Dict[str, Any],
    ) -> tuple[Dict[str, str], Dict[str, str]]:
        added_ids_by_email: Dict[str, str] = {}
        added_ids_by_linkedin: Dict[str, str] = {}

        def ingest(items: Any) -> None:
            if not isinstance(items, list):
                return
            for item in items:
                if not isinstance(item, dict):
                    continue
                provider_id = self._extract_provider_id(item)
                if not provider_id:
                    continue
                email = str(
                    item.get("email")
                    or item.get("email_address")
                    or item.get("lead_email")
                    or ""
                ).strip().lower()
                linkedin = str(
                    item.get("linkedin_url")
                    or item.get("linkedinUrl")
                    or item.get("linkedin")
                    or item.get("linkedin_profile")
                    or ""
                ).strip().lower()
                if email:
                    added_ids_by_email[email] = provider_id
                if linkedin:
                    added_ids_by_linkedin[linkedin] = provider_id

        # Common list payload variants used by sequence add endpoints.
        for container in (data, payload):
            if not isinstance(container, dict):
                continue
            for key in (
                "added_leads",
                "added_contacts",
                "added_people",
                "people_added",
                "contacts_added",
                "leads_added",
                "added",
                "results",
            ):
                ingest(container.get(key))

            map_by_email = container.get("added_by_email")
            if isinstance(map_by_email, dict):
                for email, provider_id in map_by_email.items():
                    email_key = str(email or "").strip().lower()
                    provider_id_text = str(provider_id or "").strip()
                    if email_key and provider_id_text:
                        added_ids_by_email[email_key] = provider_id_text

            map_by_linkedin = (
                container.get("added_by_linkedin")
                or container.get("added_by_linkedin_url")
            )
            if isinstance(map_by_linkedin, dict):
                for linkedin, provider_id in map_by_linkedin.items():
                    linkedin_key = str(linkedin or "").strip().lower()
                    provider_id_text = str(provider_id or "").strip()
                    if linkedin_key and provider_id_text:
                        added_ids_by_linkedin[linkedin_key] = provider_id_text

        return added_ids_by_email, added_ids_by_linkedin

    def _parse_enrollment_result(self, payload: Dict[str, Any]) -> EnrollmentResult:
        data = payload.get("data") if isinstance(payload.get("data"), dict) else payload
        if not isinstance(data, dict):
            data = {}

        skipped = data.get("skipped") if isinstance(data.get("skipped"), dict) else {}

        total = int(data.get("total") or data.get("total_leads") or 0)
        total_added = int(
            data.get("total_added")
            or data.get("total_added_to_sequence")
            or data.get("added")
            or data.get("added_count")
            or 0
        )
        duplicate_emails = list(
            data.get("duplicate_emails")
            or data.get("duplicates")
            or skipped.get("duplicate_emails")
            or []
        )
        duplicate_linkedin_urls = list(
            data.get("duplicate_linkedin_urls")
            or skipped.get("duplicate_linkedin_urls")
            or []
        )
        already_in_sequence = list(
            data.get("already_in_sequence")
            or data.get("already_in_sequence_emails")
            or data.get("already_in_sequence_and_skipped")
            or skipped.get("already_in_sequence")
            or []
        )
        excluded = list(
            data.get("excluded")
            or data.get("excluded_emails")
            or data.get("in_exclusion_list_and_skipped")
            or skipped.get("excluded")
            or []
        )
        added_ids_by_email, added_ids_by_linkedin = self._extract_added_ids(payload, data)

        return EnrollmentResult(
            total=total,
            total_added=total_added,
            duplicate_emails=[str(x) for x in duplicate_emails if x],
            duplicate_linkedin_urls=[str(x) for x in duplicate_linkedin_urls if x],
            already_in_sequence=[str(x) for x in already_in_sequence if x],
            excluded=[str(x) for x in excluded if x],
            added_ids_by_email=added_ids_by_email,
            added_ids_by_linkedin=added_ids_by_linkedin,
        )

    def _split_href(self, href: str) -> tuple[str, Dict[str, Any]]:
        parsed = urlparse(href)
        path = parsed.path or href
        params: Dict[str, Any] = {}
        if parsed.query:
            parsed_params = parse_qs(parsed.query)
            for key, value in parsed_params.items():
                if not value:
                    continue
                params[key] = value[0] if len(value) == 1 else value
        if path and not path.startswith("/") and not path.startswith("http"):
            path = f"/{path}"
        return path, params

    def _follow_pages(
        self,
        path: str,
        params: Optional[Dict[str, Any]] = None,
        page_size: Optional[int] = None,
        data_keys: tuple[str, ...] = ("data", "results"),
    ) -> Iterable[Dict[str, Any]]:
        next_path = path
        next_params = dict(params or {})
        if page_size:
            next_params.setdefault("page[size]", page_size)

        while next_path:
            resp = self._am_request("GET", next_path, params=next_params)
            payload = resp.json() if resp.content else {}
            data = []
            if isinstance(payload, dict):
                for key in data_keys:
                    candidate = payload.get(key)
                    if candidate is not None:
                        data = candidate
                        break
            if isinstance(data, list):
                for item in data:
                    yield item

            links = payload.get("_links") or payload.get("links") or {}
            next_link = links.get("next") or {}
            href = next_link.get("href") or next_link.get("url")
            if not href:
                break
            next_path, next_params = self._split_href(href)

    # ------------------------------------------------------------------
    # Public API methods
    # ------------------------------------------------------------------
    def whoami(self) -> Dict[str, Any]:
        resp = self._am_request("GET", "/account-info")
        payload = resp.json() if resp.content else {}
        return payload if isinstance(payload, dict) else {"raw": payload}

    def probe_capabilities(self) -> List[CapabilityCheck]:
        checks = [
            self._probe_endpoint("account", "/account-info"),
            self._probe_endpoint("sequences", "/sequences"),
            self._probe_endpoint("contacts", "/contacts", params={"page[size]": 1}),
        ]
        return checks

    def _probe_endpoint(
        self,
        name: str,
        path: str,
        *,
        params: Optional[Dict[str, Any]] = None,
    ) -> CapabilityCheck:
        try:
            resp = self._am_request("GET", path, params=params, allow_statuses=(403,))
            if resp.status_code == 403:
                detail = f"{resp.status_code} {resp.reason}".strip()
                return CapabilityCheck(name=name, status="denied", detail=detail)
            return CapabilityCheck(name=name, status="ok", detail="")
        except AmplemarketError as exc:
            return CapabilityCheck(name=name, status="error", detail=str(exc))

    def list_sequences(self, page_size: int = 50) -> List[AmplemarketSequence]:
        sequences: List[AmplemarketSequence] = []
        for item in self._follow_pages("/sequences", page_size=page_size):
            seq = self._parse_sequence(item)
            if seq:
                sequences.append(seq)
        return sequences

    def get_contact(self, contact_id: str) -> Optional[AmplemarketContact]:
        resp = self._am_request("GET", f"/contacts/{contact_id}", allow_statuses=(404,))
        if resp.status_code == 404:
            return None
        payload = resp.json() if resp.content else {}
        data = payload.get("data") if isinstance(payload, dict) else payload
        return self._parse_contact(data) if isinstance(data, dict) else None

    def find_contact_by_email(self, email: str) -> Optional[AmplemarketContact]:
        email_safe = quote(email)
        resp = self._am_request("GET", f"/contacts/email/{email_safe}", allow_statuses=(404,))
        if resp.status_code == 404:
            return None
        payload = resp.json() if resp.content else {}
        data = payload.get("data") if isinstance(payload, dict) else payload
        return self._parse_contact(data) if isinstance(data, dict) else None

    def list_contacts(self, limit: int = 100, page_size: int = 100) -> List[AmplemarketContact]:
        contacts: List[AmplemarketContact] = []
        for item in self._follow_pages("/contacts", page_size=page_size):
            contact = self._parse_contact(item)
            if contact:
                contacts.append(contact)
                if limit and len(contacts) >= limit:
                    break
        return contacts

    def _parse_enrichment_request(self, payload: Dict[str, Any]) -> AmplemarketEnrichmentRequest:
        data = payload.get("data") if isinstance(payload.get("data"), dict) else payload
        if not isinstance(data, dict):
            data = {}
        request_id = str(data.get("id") or data.get("request_id") or "")
        status = str(data.get("status") or data.get("state") or "")
        request_type = str(data.get("type") or data.get("request_type") or "")
        total_requested = data.get("total_requested") or data.get("total") or data.get("total_leads")
        total_enriched = data.get("total_enriched") or data.get("enriched") or data.get("total_enriched_leads")
        try:
            total_requested = int(total_requested) if total_requested is not None else None
        except (TypeError, ValueError):
            total_requested = None
        try:
            total_enriched = int(total_enriched) if total_enriched is not None else None
        except (TypeError, ValueError):
            total_enriched = None
        return AmplemarketEnrichmentRequest(
            id=request_id,
            status=status,
            request_type=request_type,
            total_requested=total_requested,
            total_enriched=total_enriched,
            raw=payload if isinstance(payload, dict) else {},
        )

    def _parse_lead_list(self, payload: Dict[str, Any]) -> Optional[AmplemarketLeadList]:
        if not isinstance(payload, dict):
            return None
        list_id = payload.get("id") or payload.get("lead_list_id") or payload.get("leadListId")
        if list_id is None:
            return None
        name = payload.get("name") or payload.get("lead_list_name") or payload.get("leadListName") or ""
        status = payload.get("status") or payload.get("state") or ""
        lead_type = payload.get("type") or payload.get("lead_type") or payload.get("leadType") or ""
        owner = payload.get("owner") if isinstance(payload.get("owner"), dict) else {}
        owner_email = owner.get("email") if owner else None
        total_leads = payload.get("total") or payload.get("total_leads") or payload.get("count")
        try:
            total_leads = int(total_leads) if total_leads is not None else None
        except (TypeError, ValueError):
            total_leads = None
        return AmplemarketLeadList(
            id=str(list_id),
            name=str(name),
            status=str(status),
            lead_type=str(lead_type),
            shared=payload.get("shared"),
            visible=payload.get("visible"),
            owner_email=owner_email,
            total_leads=total_leads,
            raw=payload,
        )

    def search_people(self, request: Dict[str, Any]) -> AmplemarketSearchPage:
        payload = _prune_payload(dict(request or {}))
        resp = self._am_request("POST", "/people/search", json=payload)
        result = resp.json() if resp.content else {}
        if not isinstance(result, dict):
            return AmplemarketSearchPage(results=[], pagination={}, raw={"raw": result})
        results = result.get("data") or result.get("people") or result.get("results") or []
        if not isinstance(results, list):
            results = []
        pagination = result.get("_pagination") or result.get("pagination") or {}
        return AmplemarketSearchPage(results=results, pagination=pagination or {}, raw=result)

    def find_person(self, **params: Any) -> Optional[Dict[str, Any]]:
        cleaned = _prune_payload(params)
        resp = self._am_request("GET", "/people/find", params=cleaned, allow_statuses=(404,))
        if resp.status_code == 404:
            return None
        payload = resp.json() if resp.content else {}
        return payload if isinstance(payload, dict) else {"raw": payload}

    def start_person_enrichment_batch(
        self,
        leads: List[Dict[str, Any]],
        *,
        reveal_email: Optional[bool] = None,
        reveal_phone_numbers: Optional[bool] = None,
    ) -> AmplemarketEnrichmentRequest:
        payload = _prune_payload(
            {
                "leads": leads,
                "reveal_email": reveal_email,
                "reveal_phone_numbers": reveal_phone_numbers,
            }
        )
        resp = self._am_request("POST", "/people/enrichment-requests", json=payload)
        data = resp.json() if resp.content else {}
        return self._parse_enrichment_request(data if isinstance(data, dict) else {})

    def get_person_enrichment_batch(
        self,
        request_id: str,
        *,
        page_size: Optional[int] = None,
        page_after: Optional[str] = None,
        page_before: Optional[str] = None,
    ) -> Dict[str, Any]:
        params: Dict[str, Any] = {}
        if page_size:
            params["page[size]"] = page_size
        if page_after:
            params["page[after]"] = page_after
        if page_before:
            params["page[before]"] = page_before
        resp = self._am_request("GET", f"/people/enrichment-requests/{request_id}", params=params)
        payload = resp.json() if resp.content else {}
        return payload if isinstance(payload, dict) else {"raw": payload}

    def cancel_person_enrichment_batch(self, request_id: str) -> Dict[str, Any]:
        resp = self._am_request(
            "PATCH",
            f"/people/enrichment-requests/{request_id}",
            json={"status": "canceled"},
        )
        payload = resp.json() if resp.content else {}
        return payload if isinstance(payload, dict) else {"raw": payload}

    def search_companies(self, request: Dict[str, Any]) -> AmplemarketSearchPage:
        """
        Search companies via POST /companies/search.

        Note: This endpoint is documented in guides but may change without notice.
        """
        payload = _prune_payload(dict(request or {}))
        resp = self._am_request("POST", "/companies/search", json=payload)
        result = resp.json() if resp.content else {}
        if not isinstance(result, dict):
            return AmplemarketSearchPage(results=[], pagination={}, raw={"raw": result})
        results = result.get("data") or result.get("companies") or result.get("results") or []
        if not isinstance(results, list):
            results = []
        pagination = result.get("_pagination") or result.get("pagination") or {}
        return AmplemarketSearchPage(results=results, pagination=pagination or {}, raw=result)

    def find_company(self, **params: Any) -> Optional[Dict[str, Any]]:
        cleaned = _prune_payload(params)
        resp = self._am_request("GET", "/companies/find", params=cleaned, allow_statuses=(404,))
        if resp.status_code == 404:
            return None
        payload = resp.json() if resp.content else {}
        return payload if isinstance(payload, dict) else {"raw": payload}

    def start_company_enrichment_batch(
        self,
        leads: List[Dict[str, Any]],
    ) -> AmplemarketEnrichmentRequest:
        payload = _prune_payload({"companies": leads})
        resp = self._am_request("POST", "/companies/enrichment-requests", json=payload)
        data = resp.json() if resp.content else {}
        return self._parse_enrichment_request(data if isinstance(data, dict) else {})

    def get_company_enrichment_batch(
        self,
        request_id: str,
        *,
        page_size: Optional[int] = None,
        page_after: Optional[str] = None,
        page_before: Optional[str] = None,
    ) -> Dict[str, Any]:
        params: Dict[str, Any] = {}
        if page_size:
            params["page[size]"] = page_size
        if page_after:
            params["page[after]"] = page_after
        if page_before:
            params["page[before]"] = page_before
        resp = self._am_request("GET", f"/companies/enrichment-requests/{request_id}", params=params)
        payload = resp.json() if resp.content else {}
        return payload if isinstance(payload, dict) else {"raw": payload}

    def cancel_company_enrichment_batch(self, request_id: str) -> Dict[str, Any]:
        resp = self._am_request(
            "PATCH",
            f"/companies/enrichment-requests/{request_id}",
            json={"status": "canceled"},
        )
        payload = resp.json() if resp.content else {}
        return payload if isinstance(payload, dict) else {"raw": payload}

    def list_lead_lists(self, limit: int = 100, page_size: int = 50) -> List[AmplemarketLeadList]:
        lead_lists: List[AmplemarketLeadList] = []
        for item in self._follow_pages("/lead-lists", page_size=page_size, data_keys=("lead_lists", "data", "results")):
            parsed = self._parse_lead_list(item)
            if parsed:
                lead_lists.append(parsed)
                if limit and len(lead_lists) >= limit:
                    break
        return lead_lists

    def create_lead_list(
        self,
        *,
        name: str,
        lead_type: str,
        leads: List[Dict[str, Any]],
        owner_email: Optional[str] = None,
        shared: Optional[bool] = None,
        visible: Optional[bool] = None,
    ) -> AmplemarketLeadList:
        normalized_type = LEAD_LIST_TYPE_ALIASES.get(lead_type, lead_type)
        normalized_type = str(normalized_type or "").strip().lower()
        if normalized_type not in LEAD_LIST_TYPES:
            raise UnsupportedValueError(f"Unsupported lead list type: {lead_type}")
        self._validate_lead_list_entries(leads, normalized_type)
        payload = _prune_payload(
            {
                "name": name,
                "type": normalized_type,
                "owner": {"email": owner_email} if owner_email else None,
                "shared": shared,
                "visible": visible,
                "leads": leads,
            }
        )
        resp = self._am_request("POST", "/lead-lists", json=payload)
        data = resp.json() if resp.content else {}
        parsed = self._parse_lead_list(data if isinstance(data, dict) else {})
        if parsed:
            return parsed
        raise AmplemarketError("Unexpected response while creating lead list.")

    def get_lead_list(
        self,
        list_id: str,
        *,
        page_size: Optional[int] = None,
        page_after: Optional[str] = None,
        page_before: Optional[str] = None,
    ) -> Dict[str, Any]:
        params: Dict[str, Any] = {}
        if page_size:
            params["page[size]"] = page_size
        if page_after:
            params["page[after]"] = page_after
        if page_before:
            params["page[before]"] = page_before
        resp = self._am_request("GET", f"/lead-lists/{list_id}", params=params)
        payload = resp.json() if resp.content else {}
        return payload if isinstance(payload, dict) else {"raw": payload}

    def add_leads_to_list(
        self,
        list_id: str,
        leads: List[Dict[str, Any]],
        *,
        lead_type: str,
        chunk_size: int = LEAD_LIST_BATCH_MAX,
        max_total: int = LEAD_LIST_MAX,
    ) -> Dict[str, Any]:
        normalized_type = LEAD_LIST_TYPE_ALIASES.get(lead_type, lead_type)
        normalized_type = str(normalized_type or "").strip().lower()
        if normalized_type not in LEAD_LIST_TYPES:
            raise UnsupportedValueError(f"Unsupported lead list type: {lead_type}")
        if len(leads) > max_total:
            raise AmplemarketError(f"Lead list supports up to {max_total} leads.")
        self._validate_lead_list_entries(leads, normalized_type)
        chunk_size = min(max(chunk_size, 1), LEAD_LIST_BATCH_MAX)

        def _chunk(items: List[Dict[str, Any]], size: int) -> Iterable[List[Dict[str, Any]]]:
            for i in range(0, len(items), size):
                yield items[i : i + size]

        responses: List[Dict[str, Any]] = []
        total_added = 0
        total_submitted = 0
        chunks = 0
        for chunk in _chunk(leads, chunk_size):
            payload = _prune_payload(
                {
                    "type": normalized_type,
                    "leads": chunk,
                }
            )
            resp = self._am_request("POST", f"/lead-lists/{list_id}/leads", json=payload)
            data = resp.json() if resp.content else {}
            if isinstance(data, dict):
                responses.append(data)
                total_submitted += int(data.get("total") or len(chunk))
                total_added += int(data.get("total_added_to_lead_list") or data.get("total_added") or 0)
            else:
                responses.append({"raw": data})
                total_submitted += len(chunk)
            chunks += 1

        return {
            "list_id": list_id,
            "lead_type": normalized_type,
            "total_submitted": total_submitted,
            "total_added": total_added,
            "chunks": chunks,
            "responses": responses,
        }

    def _extract_status(self, payload: Dict[str, Any]) -> str:
        if not isinstance(payload, dict):
            return ""
        if isinstance(payload.get("data"), dict):
            status = payload["data"].get("status") or payload["data"].get("state")
            if status:
                return str(status)
        status = payload.get("status") or payload.get("state") or payload.get("request_status")
        return str(status) if status else ""

    def _validate_lead_list_entries(self, leads: List[Dict[str, Any]], lead_type: str) -> None:
        invalid_indices: List[int] = []
        for idx, lead in enumerate(leads):
            if not isinstance(lead, dict):
                invalid_indices.append(idx + 1)
                continue
            if lead_type == "linkedin":
                if not lead.get("linkedin_url"):
                    invalid_indices.append(idx + 1)
            elif lead_type == "email":
                if not lead.get("email"):
                    invalid_indices.append(idx + 1)
            elif lead_type == "titles_and_company":
                if not lead.get("title") or not (lead.get("company_name") or lead.get("company_domain")):
                    invalid_indices.append(idx + 1)

        if invalid_indices:
            sample = ", ".join(str(i) for i in invalid_indices[:10])
            raise MissingFieldError(
                f"{len(invalid_indices)} invalid leads detected (rows: {sample}). "
                "Check required fields for the lead list type."
            )

    def wait_for_enrichment(
        self,
        path: str,
        *,
        poll_interval: float = 30.0,
        max_wait: Optional[float] = None,
    ) -> Dict[str, Any]:
        start = time.monotonic()
        while True:
            resp = self._am_request("GET", path)
            payload = resp.json() if resp.content else {}
            if not isinstance(payload, dict):
                return {"raw": payload}
            status = self._extract_status(payload).lower()
            if status and status in {"completed", "failed", "canceled", "cancelled", "done", "finished"}:
                return payload
            if status and "complete" in status:
                return payload
            if max_wait is not None and (time.monotonic() - start) >= max_wait:
                return payload
            retry_after = self._retry_after_seconds(resp)
            delay = retry_after if retry_after is not None else poll_interval
            if delay > 0:
                self._sleep(delay)

    def add_to_sequence(
        self,
        sequence_id: str,
        leads: List[AmplemarketContact | AmplemarketSequenceLeadPayload | Dict[str, Any]],
        *,
        overrides: Optional[Dict[str, Any]] = None,
        settings: Optional[Dict[str, Any]] = None,
        leads_distribution: Optional[str] = None,
        mailboxes: Optional[List[str]] = None,
        scheduler_start_time: Optional[str] = None,
        ignore_recently_contacted: Optional[bool] = None,
        ignore_exclusion_list: Optional[bool] = None,
        ignore_duplicate_leads_in_other_draft_sequences: Optional[bool] = None,
        ignore_duplicate_leads_in_other_active_sequences: Optional[bool] = None,
        skip_email_validation: Optional[bool] = None,
        skip_email_checks: Optional[bool] = None,
        skip_phone_checks: Optional[bool] = None,
    ) -> EnrollmentResult:
        payload_leads: List[Dict[str, Any]] = []
        for lead in leads:
            payload = self._coerce_sequence_lead(lead)
            if payload:
                payload_leads.append(payload)
        if not payload_leads:
            raise AmplemarketError("No valid leads provided for enrollment.")
        if len(payload_leads) > SEQUENCE_LEADS_MAX:
            raise AmplemarketError(f"Sequence enrollment supports up to {SEQUENCE_LEADS_MAX} leads per request.")

        overrides_payload: Dict[str, Any] = dict(overrides or {})
        if ignore_recently_contacted is not None:
            overrides_payload["ignore_recently_contacted"] = ignore_recently_contacted
        if ignore_exclusion_list is not None:
            overrides_payload["ignore_exclusion_list"] = ignore_exclusion_list
        if ignore_duplicate_leads_in_other_draft_sequences is not None:
            overrides_payload["ignore_duplicate_leads_in_other_draft_sequences"] = (
                ignore_duplicate_leads_in_other_draft_sequences
            )
        if ignore_duplicate_leads_in_other_active_sequences is not None:
            overrides_payload["ignore_duplicate_leads_in_other_active_sequences"] = (
                ignore_duplicate_leads_in_other_active_sequences
            )

        settings_payload: Dict[str, Any] = dict(settings or {})
        if leads_distribution is not None:
            settings_payload["leads_distribution"] = leads_distribution
        if mailboxes:
            settings_payload["mailboxes"] = mailboxes
        if scheduler_start_time is not None:
            settings_payload["scheduler_start_time"] = scheduler_start_time
        if skip_email_validation is not None:
            settings_payload["skip_email_validation"] = skip_email_validation
        if skip_email_checks is not None:
            settings_payload["skip_email_checks"] = skip_email_checks
        if skip_phone_checks is not None:
            settings_payload["skip_phone_checks"] = skip_phone_checks

        resp = self._am_request(
            "POST",
            f"/sequences/{sequence_id}/leads",
            json=_prune_payload(
                {
                    "leads": payload_leads,
                    "overrides": overrides_payload,
                    "settings": settings_payload,
                }
            ),
        )
        payload = resp.json() if resp.content else {}
        return self._parse_enrollment_result(payload if isinstance(payload, dict) else {})

    def add_to_sequence_bulk(
        self,
        leads: List[AmplemarketContact | AmplemarketSequenceLeadPayload | Dict[str, Any]],
        sequence_id: str,
        pacing_profile: AmplemarketPacingProfile | None = None,
        *,
        overrides: Optional[Dict[str, Any]] = None,
        settings: Optional[Dict[str, Any]] = None,
        leads_distribution: Optional[str] = None,
        mailboxes: Optional[List[str]] = None,
        scheduler_start_time: Optional[str] = None,
        ignore_recently_contacted: Optional[bool] = None,
        ignore_exclusion_list: Optional[bool] = None,
        ignore_duplicate_leads_in_other_draft_sequences: Optional[bool] = None,
        ignore_duplicate_leads_in_other_active_sequences: Optional[bool] = None,
        skip_email_validation: Optional[bool] = None,
        skip_email_checks: Optional[bool] = None,
        skip_phone_checks: Optional[bool] = None,
    ) -> BatchOutcome:
        outcome = BatchOutcome()
        profile = pacing_profile or get_amplemarket_profile("safe")
        downshifted = False
        leads_list: List[AmplemarketContact | Dict[str, Any]] = list(leads)

        idx = 0
        total_leads = len(leads_list)
        while idx < total_leads:
            batch_size_limit = min(profile.leads_per_request, SEQUENCE_LEADS_MAX)
            batch = leads_list[idx : idx + batch_size_limit]
            batch_size = len(batch)
            if batch_size == 0:
                break
            batch_payloads: List[Dict[str, Any]] = []
            batch_identifiers: List[Dict[str, Optional[str]]] = []

            for lead in batch:
                try:
                    payload = self._coerce_sequence_lead(lead)
                except AmplemarketError as exc:
                    outcome.failed.append({"email": None, "error": str(exc)})
                    continue
                batch_payloads.append(payload)
                batch_identifiers.append(
                    {
                        "email": payload.get("email"),
                        "linkedin_url": payload.get("linkedin_url"),
                    }
                )

            if not batch_payloads:
                idx += batch_size
                continue

            try:
                result = self.add_to_sequence(
                    sequence_id,
                    batch_payloads,
                    overrides=overrides,
                    settings=settings,
                    leads_distribution=leads_distribution,
                    mailboxes=mailboxes,
                    scheduler_start_time=scheduler_start_time,
                    ignore_recently_contacted=ignore_recently_contacted,
                    ignore_exclusion_list=ignore_exclusion_list,
                    ignore_duplicate_leads_in_other_draft_sequences=ignore_duplicate_leads_in_other_draft_sequences,
                    ignore_duplicate_leads_in_other_active_sequences=ignore_duplicate_leads_in_other_active_sequences,
                    skip_email_validation=skip_email_validation,
                    skip_email_checks=skip_email_checks,
                    skip_phone_checks=skip_phone_checks,
                )
            except AmplemarketRateLimitError as exc:
                if profile.name == "turbo" and not downshifted:
                    logger.warning("Amplemarket rate limited; downshifting to safe pacing.")
                    profile = get_amplemarket_profile("safe")
                    downshifted = True
                for identifier in batch_identifiers:
                    outcome.retryable_failed.append(
                        {
                            "email": identifier.get("email"),
                            "linkedin_url": identifier.get("linkedin_url"),
                            "error": str(exc),
                            "status": "rate_limited",
                        }
                    )
                if profile.inter_call_delay > 0:
                    self._sleep(profile.inter_call_delay)
                idx += batch_size
                continue
            duplicate_set = set(result.duplicate_emails) | set(result.duplicate_linkedin_urls)
            excluded_set = set(result.excluded) | set(result.already_in_sequence)

            for identifier in batch_identifiers:
                email = identifier.get("email")
                linkedin = identifier.get("linkedin_url")
                if (email and email in duplicate_set) or (linkedin and linkedin in duplicate_set):
                    outcome.retryable_failed.append(
                        {"email": email, "linkedin_url": linkedin, "status": "duplicate"}
                    )
                    continue
                if (email and email in excluded_set) or (linkedin and linkedin in excluded_set):
                    outcome.failed.append({"email": email, "linkedin_url": linkedin, "status": "excluded"})
                    continue
                outcome.succeeded.append({"email": email, "linkedin_url": linkedin, "status": "added"})

            if profile.inter_call_delay > 0:
                self._sleep(profile.inter_call_delay)
            idx += batch_size

        return outcome


def _load_contacts_from_csv(csv_path: str, email_col: str) -> List[AmplemarketContact]:
    path = Path(csv_path)
    if not path.exists():
        raise AmplemarketError(f"CSV file not found: {csv_path}")

    contacts: List[AmplemarketContact] = []
    with path.open(newline="", encoding="utf-8") as handle:
        reader = csv.DictReader(handle)
        for row in reader:
            email = (row.get(email_col) or "").strip()
            if not email:
                continue
            contacts.append(
                AmplemarketContact(
                    email=email,
                    first_name=(row.get("first_name") or "").strip() or None,
                    last_name=(row.get("last_name") or "").strip() or None,
                    title=(row.get("title") or "").strip() or None,
                    company_name=(row.get("company_name") or "").strip() or None,
                    linkedin_url=(row.get("linkedin_url") or "").strip() or None,
                    phone=(row.get("phone") or "").strip() or None,
                )
            )

    if not contacts:
        raise AmplemarketError("No valid contacts found in CSV.")

    return contacts


def _load_sequence_leads_from_csv(
    csv_path: str,
    email_col: str = "email",
    linkedin_col: Optional[str] = "linkedin_url",
    data_columns: Optional[List[str]] = None,
) -> List[Dict[str, Any]]:
    path = Path(csv_path)
    if not path.exists():
        raise AmplemarketError(f"CSV file not found: {csv_path}")

    data_columns = data_columns or []
    leads: List[Dict[str, Any]] = []
    with path.open(newline="", encoding="utf-8") as handle:
        reader = csv.DictReader(handle)
        for row in reader:
            email = (row.get(email_col) or "").strip()
            linkedin_url = (row.get(linkedin_col) or "").strip() if linkedin_col else ""
            lead: Dict[str, Any] = {
                "email": email or None,
                "linkedin_url": linkedin_url or None,
                "first_name": (row.get("first_name") or "").strip() or None,
                "last_name": (row.get("last_name") or "").strip() or None,
                "title": (row.get("title") or "").strip() or None,
                "company_name": (row.get("company_name") or "").strip() or None,
                "phone": (row.get("phone") or "").strip() or None,
            }
            data: Dict[str, Any] = {}
            for col in data_columns:
                value = (row.get(col) or "").strip()
                if value:
                    data[col] = value
            if data:
                lead["data"] = data
            lead = _prune_payload(lead)
            if not lead.get("email") and not lead.get("linkedin_url"):
                continue
            leads.append(lead)

    if not leads:
        raise AmplemarketError("No valid leads found in CSV.")
    return leads


def enroll_from_csv(
    csv_path: str,
    sequence_id: str,
    email_col: str = "email",
    linkedin_col: Optional[str] = None,
    data_columns: Optional[List[str]] = None,
    pacing_profile: AmplemarketPacingProfile | None = None,
) -> BatchOutcome:
    leads = _load_sequence_leads_from_csv(
        csv_path=csv_path,
        email_col=email_col,
        linkedin_col=linkedin_col,
        data_columns=data_columns,
    )
    client = AmplemarketClient()
    return client.add_to_sequence_bulk(
        leads,
        sequence_id=sequence_id,
        pacing_profile=pacing_profile,
    )


__all__ = [
    "AmplemarketError",
    "AmplemarketAuthError",
    "AmplemarketRateLimitError",
    "InsufficientCreditsError",
    "MissingFieldError",
    "UnsupportedValueError",
    "AmplemarketContact",
    "AmplemarketSequence",
    "AmplemarketLeadList",
    "AmplemarketEnrichmentRequest",
    "AmplemarketSearchPage",
    "AmplemarketSequenceLeadPayload",
    "EnrollmentResult",
    "AmplemarketClient",
    "get_amplemarket_api_key",
    "enroll_from_csv",
]
