"""Sequencer export worker for Amplemarket (Sheets add-on)."""

from __future__ import annotations

import asyncio
import json
import logging
import re
from dataclasses import dataclass
from datetime import datetime, timezone
from typing import Any, Dict, List, Optional

from g_gremlin.amplemarket import (
    AmplemarketAuthError,
    AmplemarketClient,
    AmplemarketError,
    AmplemarketRateLimitError,
)
from g_gremlin.pacing import get_amplemarket_profile

from ..api.progress import get_redis
from ..db import AsyncSessionLocal
from ..services.secret_storage import get_saas_secret_backend
from .sequencer_worker import (
    ERRORS_KEY,
    PROGRESS_KEY,
    RESULTS_KEY,
    RESULT_TTL,
    STATE_KEY,
    SUMMARY_KEY,
)

logger = logging.getLogger(__name__)

MAX_RETRIES = 3
EMAIL_RE = re.compile(r"^[^@\s]+@[^@\s]+\.[^@\s]+$")
SEQUENCE_DATA_KEY_RE = re.compile(r"^[a-z][a-z0-9_]{0,254}$")
SEQUENCE_DATA_FIELDS_MAX = 50


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


def _normalize_status(value: Any) -> str:
    return _clean_string(value).lower()


def _get_row_number(row: Dict[str, Any], fallback_index: int) -> int:
    for key, is_zero_based in (
        ("row_number", False),
        ("row_index", True),
        ("__fm_row_index__", True),
    ):
        if key in row:
            try:
                num = int(row.get(key))
                return num + (1 if is_zero_based else 0)
            except (TypeError, ValueError):
                continue
    return fallback_index + 1


def _is_valid_email(value: Any) -> bool:
    text = _clean_string(value)
    if not text:
        return False
    return bool(EMAIL_RE.match(text))


def _should_skip_row(row: Dict[str, Any], campaign_id: str, writeback: Dict[str, Any]) -> bool:
    if not writeback or not writeback.get("enabled", True):
        return False
    status_col = writeback.get("status_column")
    campaign_col = writeback.get("campaign_column")
    if not status_col or not campaign_col:
        return False
    status_val = _normalize_status(row.get(status_col))
    campaign_val = _clean_string(row.get(campaign_col))
    return status_val in {"enrolled", "exists"} and campaign_val == campaign_id


@dataclass
class RowContext:
    row_number: int
    email: Optional[str]
    linkedin_url: Optional[str]
    lead: Dict[str, Any]


class AmplemarketSequencerWorker:
    def __init__(
        self,
        account_id: str,
        job_id: str,
        config: Dict[str, Any],
        rows: List[Dict[str, Any]],
        redis=None,
        sleep: Optional[callable] = None,
    ) -> None:
        self.account_id = account_id
        self.job_id = job_id
        self.config = config
        self.rows = rows
        self.redis = redis
        self.sleep = sleep or asyncio.sleep
        self.summary = {
            "total_rows": len(rows),
            "processed_rows": 0,
            "enrolled": 0,
            "exists": 0,
            "skipped": 0,
            "failed": 0,
        }
        self.errors: List[Dict[str, Any]] = []
        self.errors_truncated = False

    async def _get_redis(self):
        if self.redis is not None:
            return self.redis
        return await get_redis()

    async def _set_state(self, state: str) -> None:
        redis = await self._get_redis()
        if not redis:
            return
        await redis.set(STATE_KEY.format(job_id=self.job_id), state, ex=RESULT_TTL)

    async def _set_progress(self, progress: int) -> None:
        redis = await self._get_redis()
        if not redis:
            return
        await redis.set(PROGRESS_KEY.format(job_id=self.job_id), str(progress), ex=RESULT_TTL)

    async def _set_summary(self) -> None:
        redis = await self._get_redis()
        if not redis:
            return
        await redis.set(SUMMARY_KEY.format(job_id=self.job_id), json.dumps(self.summary), ex=RESULT_TTL)

    async def _set_errors(self) -> None:
        redis = await self._get_redis()
        if not redis:
            return
        payload = {"errors": self.errors, "errors_truncated": self.errors_truncated}
        await redis.set(ERRORS_KEY.format(job_id=self.job_id), json.dumps(payload), ex=RESULT_TTL)

    async def _store_result(self, result: Dict[str, Any]) -> None:
        redis = await self._get_redis()
        if not redis:
            return
        await redis.rpush(RESULTS_KEY.format(job_id=self.job_id), json.dumps(result))
        await redis.expire(RESULTS_KEY.format(job_id=self.job_id), RESULT_TTL)

    def _record_error(self, row_number: int, status: str, error: str) -> None:
        if len(self.errors) >= 50:
            self.errors_truncated = True
            return
        self.errors.append({"row_number": row_number, "status": status, "error": error})

    def _increment_summary(self, status: str) -> None:
        if status in self.summary:
            self.summary[status] += 1
        self.summary["processed_rows"] = (
            self.summary["enrolled"]
            + self.summary["exists"]
            + self.summary["skipped"]
            + self.summary["failed"]
        )

    async def _resolve_api_key(self) -> str:
        profile_id = self.config.get("profile_id")
        if not profile_id:
            raise AmplemarketAuthError("Profile not configured")
        async with AsyncSessionLocal() as db:
            backend = get_saas_secret_backend()
            secret = await backend.get_secret_by_id(
                db=db,
                secret_id=profile_id,
                workspace_id=self.account_id,
                integration="amplemarket",
            )
        if not secret or not secret.value:
            raise AmplemarketAuthError("Amplemarket credentials not configured")
        return str(secret.value)

    def _build_lead(self, row: Dict[str, Any], mapping: Dict[str, Any]) -> tuple[Dict[str, Any], Optional[str], Optional[str]]:
        def value_for(field: str) -> Optional[str]:
            column = mapping.get(field)
            if not column:
                return None
            text = _clean_string(row.get(column))
            return text if text else None

        raw_email = value_for("email")
        raw_linkedin = value_for("linkedin_url")

        email = raw_email if raw_email and _is_valid_email(raw_email) else None
        linkedin_url = raw_linkedin if raw_linkedin else None

        lead: Dict[str, Any] = {}
        if email:
            lead["email"] = email
        if linkedin_url:
            lead["linkedin_url"] = linkedin_url

        first_name = value_for("first_name")
        last_name = value_for("last_name")
        title = value_for("title")
        phone = value_for("phone")
        company = value_for("company")

        if first_name:
            lead["first_name"] = first_name
        if last_name:
            lead["last_name"] = last_name
        if title:
            lead["title"] = title
        if phone:
            lead["phone"] = phone
        if company:
            lead["company_name"] = company

        custom_vars = mapping.get("custom_vars") or {}
        if isinstance(custom_vars, dict) and custom_vars:
            data: Dict[str, Any] = {}
            for key, column in custom_vars.items():
                if not key or not column:
                    continue
                key_str = str(key).strip()
                if not key_str or not SEQUENCE_DATA_KEY_RE.match(key_str):
                    continue
                value = _clean_string(row.get(column))
                if value:
                    data[key_str] = value
                if len(data) >= SEQUENCE_DATA_FIELDS_MAX:
                    break
            if data:
                lead["data"] = data

        return lead, email, linkedin_url

    async def _add_to_sequence_with_retry(
        self,
        client: AmplemarketClient,
        sequence_id: str,
        leads: List[Dict[str, Any]],
    ):
        attempt = 0
        while True:
            try:
                return await asyncio.to_thread(client.add_to_sequence, sequence_id, leads)
            except AmplemarketRateLimitError as exc:
                attempt += 1
                if attempt >= MAX_RETRIES:
                    raise
                delay = exc.retry_after if exc.retry_after is not None else min(30.0, float(2 ** attempt))
                await self.sleep(max(1.0, delay))
            except AmplemarketError:
                attempt += 1
                if attempt >= MAX_RETRIES:
                    raise
                await self.sleep(float(2 ** (attempt - 1)))

    async def run(self) -> None:
        await self._set_state("running")
        await self._set_progress(0)

        sequence_id = _clean_string(self.config.get("campaign_id"))
        mapping = self.config.get("mapping") or {}
        writeback = self.config.get("writeback") or {}

        if not sequence_id:
            self._record_error(0, "failed", "Missing campaign_id")
            await self._set_errors()
            await self._set_state("failed")
            return

        try:
            api_key = await self._resolve_api_key()
        except AmplemarketAuthError as exc:
            self._record_error(0, "failed", str(exc))
            await self._set_errors()
            await self._set_state("failed")
            return

        pending: List[RowContext] = []
        for idx, row in enumerate(self.rows):
            row_number = _get_row_number(row, idx)
            if _should_skip_row(row, sequence_id, writeback):
                await self._store_result(
                    {
                        "row_number": row_number,
                        "status": "skipped",
                        "campaign_id": sequence_id,
                        "lead_id": None,
                        "pushed_at": None,
                        "error": None,
                    }
                )
                self._increment_summary("skipped")
                continue

            email_column = mapping.get("email")
            linkedin_column = mapping.get("linkedin_url")
            raw_email = _clean_string(row.get(email_column)) if email_column else ""
            raw_linkedin = _clean_string(row.get(linkedin_column)) if linkedin_column else ""

            lead, email, linkedin_url = self._build_lead(row, mapping)
            if not email and not linkedin_url:
                if raw_email and not _is_valid_email(raw_email) and not raw_linkedin:
                    error = "Invalid email format"
                else:
                    error = "Missing required field"
                await self._store_result(
                    {
                        "row_number": row_number,
                        "status": "failed",
                        "campaign_id": sequence_id,
                        "lead_id": None,
                        "pushed_at": None,
                        "error": error,
                    }
                )
                self._increment_summary("failed")
                self._record_error(row_number, "failed", error)
                continue

            pending.append(RowContext(row_number=row_number, email=email, linkedin_url=linkedin_url, lead=lead))

        await self._set_summary()
        await self._set_errors()

        total = self.summary["total_rows"]
        profile = get_amplemarket_profile("safe")
        batch_size = max(1, int(profile.leads_per_request or 10))
        inter_delay = max(0.0, float(profile.inter_call_delay or 0.0))

        client = AmplemarketClient(api_key=api_key)
        next_index = 0
        try:
            for start in range(0, len(pending), batch_size):
                next_index = start
                batch = pending[start : start + batch_size]
                leads = [ctx.lead for ctx in batch]

                if inter_delay and start > 0:
                    await self.sleep(inter_delay)

                result = await self._add_to_sequence_with_retry(client, sequence_id, leads)
                timestamp = datetime.now(timezone.utc).isoformat()

                duplicate_emails = {str(x).strip().lower() for x in (result.duplicate_emails or []) if x}
                duplicate_linkedin = {str(x).strip().lower() for x in (result.duplicate_linkedin_urls or []) if x}
                already_in_sequence = {str(x).strip().lower() for x in (result.already_in_sequence or []) if x}
                excluded = {str(x).strip().lower() for x in (result.excluded or []) if x}

                for ctx in batch:
                    email_norm = (ctx.email or "").strip().lower()
                    linkedin_norm = (ctx.linkedin_url or "").strip().lower()

                    is_duplicate = (email_norm and email_norm in duplicate_emails) or (linkedin_norm and linkedin_norm in duplicate_linkedin)
                    is_already = (email_norm and email_norm in already_in_sequence) or (linkedin_norm and linkedin_norm in already_in_sequence)
                    is_excluded = (email_norm and email_norm in excluded) or (linkedin_norm and linkedin_norm in excluded)

                    if is_duplicate:
                        status = "exists"
                        error = None
                    elif is_already:
                        status = "exists"
                        error = None
                    elif is_excluded:
                        status = "failed"
                        error = "Excluded by Amplemarket"
                    else:
                        status = "enrolled"
                        error = None

                    pushed_at = timestamp if status in {"enrolled", "exists"} else None
                    await self._store_result(
                        {
                            "row_number": ctx.row_number,
                            "status": status,
                            "campaign_id": sequence_id,
                            "lead_id": None,
                            "pushed_at": pushed_at,
                            "error": error,
                        }
                    )

                    if status == "enrolled":
                        self._increment_summary("enrolled")
                    elif status == "exists":
                        self._increment_summary("exists")
                    elif status == "skipped":
                        self._increment_summary("skipped")
                    else:
                        self._increment_summary("failed")
                        self._record_error(ctx.row_number, "failed", error or "Unknown error")

                next_index = start + len(batch)
                await self._set_summary()
                await self._set_errors()

                processed = self.summary["processed_rows"]
                progress = int(min(99, (processed / total) * 100)) if total else 100
                await self._set_progress(progress)

            await self._set_progress(100)
            await self._set_state("completed")
        except AmplemarketAuthError as exc:
            logger.error("Amplemarket auth failure: %s", exc)
            for ctx in pending[next_index:]:
                await self._store_result(
                    {
                        "row_number": ctx.row_number,
                        "status": "failed",
                        "campaign_id": sequence_id,
                        "lead_id": None,
                        "pushed_at": None,
                        "error": str(exc),
                    }
                )
                self._increment_summary("failed")
                self._record_error(ctx.row_number, "failed", str(exc))
            await self._set_summary()
            await self._set_errors()
            await self._set_progress(100)
            await self._set_state("failed")
        except Exception as exc:  # noqa: BLE001
            logger.exception("Amplemarket sequencer export failed: %s", exc)
            for ctx in pending[next_index:]:
                await self._store_result(
                    {
                        "row_number": ctx.row_number,
                        "status": "failed",
                        "campaign_id": sequence_id,
                        "lead_id": None,
                        "pushed_at": None,
                        "error": str(exc),
                    }
                )
                self._increment_summary("failed")
                self._record_error(ctx.row_number, "failed", str(exc))
            await self._set_summary()
            await self._set_errors()
            await self._set_progress(100)
            await self._set_state("failed")
        finally:
            try:
                if getattr(client, "_session", None) is not None:
                    client._session.close()
            except Exception:  # noqa: BLE001
                logger.debug("Failed to close Amplemarket client", exc_info=True)
