"""Sequencer row-contract helpers for deterministic Sheets writeback."""

from __future__ import annotations

import re
from typing import Any, Dict, Iterable, List, Sequence, Tuple

ROW_CONTRACT_COLUMNS: Tuple[str, ...] = (
    "submission_status",
    "submission_reason",
    "provider_lead_id",
    "campaign_id",
    "pushed_at",
    "submission_error",
)

ROW_CONTRACT_STATUS_VALUES = {"enrolled", "exists", "skipped", "failed", "pending"}

ROW_CONTRACT_REASON_CODES = {
    "enrolled_new",
    "enrolled_reactivated",
    "duplicate_email",
    "duplicate_linkedin",
    "already_in_sequence",
    "exists_in_another_campaign",
    "missing_required_field",
    "invalid_email_format",
    "invalid_linkedin_url",
    "do_not_contact",
    "previously_enrolled",
    "filtered_by_sender_limit",
    "no_sender_assigned",
    "provider_rate_limited",
    "provider_auth_error",
    "provider_timeout",
    "provider_server_error",
    "provider_validation_error",
    "provider_campaign_not_found",
    "provider_campaign_paused",
    "provider_quota_exceeded",
    "batch_aborted",
}

HTTP_STATUS_RE = re.compile(r"\b([1-5]\d{2})\b")


def clean_string(value: Any) -> str:
    if value is None:
        return ""
    if isinstance(value, str):
        return value.strip()
    return str(value).strip()


def truncate_submission_error(value: Any, max_length: int = 500) -> str:
    text = clean_string(value)
    if not text:
        return ""
    if len(text) <= max_length:
        return text
    return text[:max_length]


def normalize_submission_status(value: Any) -> str:
    status = clean_string(value).lower()
    if status in ROW_CONTRACT_STATUS_VALUES:
        return status
    if status in {"duplicate", "already_in_sequence", "exists_in_campaign"}:
        return "exists"
    if status in {"queued"}:
        return "pending"
    if status in {"error"}:
        return "failed"
    return "failed"


def normalize_reason_codes(codes: Iterable[str]) -> List[str]:
    normalized: List[str] = []
    seen = set()
    for code in codes:
        text = clean_string(code)
        if not text:
            continue
        if text not in ROW_CONTRACT_REASON_CODES:
            continue
        if text in seen:
            continue
        seen.add(text)
        normalized.append(text)
    return normalized


def encode_reason_codes(codes: Iterable[str]) -> str:
    return ";".join(normalize_reason_codes(codes))


def infer_provider_failure_reason(error: Any) -> str:
    message = clean_string(error).lower()
    if not message:
        return "provider_validation_error"

    status_match = HTTP_STATUS_RE.search(message)
    status_code = int(status_match.group(1)) if status_match else None

    if status_code == 429 or "rate limit" in message:
        return "provider_rate_limited"
    if status_code in {401, 403}:
        return "provider_auth_error"
    if status_code in {408, 504}:
        return "provider_timeout"
    if status_code == 404:
        return "provider_campaign_not_found"
    if status_code in {500, 502, 503}:
        return "provider_server_error"

    if (
        "unauthoriz" in message
        or "forbidden" in message
        or "invalid credential" in message
        or "credential expired" in message
        or "token expired" in message
        or "access denied" in message
        or "authentication failed" in message
        or "authorization failed" in message
        or "api key is invalid" in message
        or "oauth" in message
        or "credential" in message
        or "expired" in message
    ):
        return "provider_auth_error"
    if "timeout" in message or "timed out" in message:
        return "provider_timeout"
    if "campaign not found" in message or "sequence not found" in message:
        return "provider_campaign_not_found"
    if "campaign paused" in message or "archived" in message or "campaign inactive" in message:
        return "provider_campaign_paused"
    if "quota" in message or "plan limit" in message or "account limit" in message:
        return "provider_quota_exceeded"
    if "server error" in message or "internal server" in message or "bad gateway" in message:
        return "provider_server_error"
    return "provider_validation_error"


def map_provider_outcome(
    provider_name: str,
    provider_status: Any,
    provider_error: Any = None,
) -> tuple[str, List[str]]:
    provider = clean_string(provider_name).lower()
    status = clean_string(provider_status).lower()
    error = clean_string(provider_error).lower()

    if status in {"enrolled", "added", "success"}:
        return "enrolled", ["enrolled_new"]

    if status in {"exists", "duplicate"}:
        if provider in {"apollo", "outreach"}:
            return "exists", ["already_in_sequence"]
        return "exists", ["duplicate_email"]

    if status == "skipped":
        if provider in {"apollo", "outreach"}:
            return "exists", ["already_in_sequence"]
        if provider in {"smartlead", "instantly"}:
            return "exists", ["duplicate_email"]
        if "excluded" in error or "suppression" in error or "dnc" in error:
            return "skipped", ["do_not_contact"]
        return "skipped", []

    if status in {"excluded"}:
        return "skipped", ["do_not_contact"]

    if status == "pending":
        return "pending", []

    if status in {"failed", "error"}:
        return "failed", [infer_provider_failure_reason(provider_error)]

    return "failed", [infer_provider_failure_reason(provider_error)]


def build_submission_result(
    *,
    row_number: int,
    campaign_id: str,
    submission_status: str,
    reason_codes: Sequence[str] | None = None,
    provider_lead_id: Any = None,
    pushed_at: Any = None,
    submission_error: Any = None,
) -> Dict[str, Any]:
    status = normalize_submission_status(submission_status)
    reasons = encode_reason_codes(reason_codes or [])
    lead_id = clean_string(provider_lead_id)
    pushed = clean_string(pushed_at)
    error = truncate_submission_error(submission_error)

    result: Dict[str, Any] = {
        "row_number": int(row_number),
        "submission_status": status,
        "submission_reason": reasons,
        "provider_lead_id": lead_id,
        "campaign_id": clean_string(campaign_id),
        "pushed_at": pushed,
        "submission_error": error,
        # Legacy aliases for transitional client compatibility.
        "status": status,
        "lead_id": lead_id or None,
        "error": error or None,
    }
    return result
