"""Async worker for pushing contacts to JustCall Sales Dialer campaigns."""

from __future__ import annotations

import asyncio
import csv
import io
import json
import logging
import random
from dataclasses import dataclass
from datetime import datetime, timezone
from typing import Any, Dict, List, Optional, Tuple

from g_gremlin.justcall import normalize_phone_e164

from ..api.progress import get_redis
from ..db import AsyncSessionLocal
from ..services.activation_utils import (
    clean_string,
    normalize_status,
    normalize_header_key,
    get_row_number,
    should_skip_row,
)
from ..services.justcall_adapter import (
    CampaignContact,
    JustCallAdapter,
    JustCallAuthError,
    JustCallError,
    JustCallRateLimitError,
)
from ..services.justcall_models import JustCallConfig
from ..services.secret_storage import get_saas_secret_backend
from ..services.email_notifications import send_justcall_complete_notification
from ..storage import get_storage_backend

logger = logging.getLogger(__name__)

META_KEY = "justcall:job:{job_id}:meta"
STATE_KEY = "justcall:job:{job_id}:state"
PROGRESS_KEY = "justcall:job:{job_id}:progress"
SUMMARY_KEY = "justcall:job:{job_id}:summary"
ERRORS_KEY = "justcall:job:{job_id}:errors"
RESULTS_KEY = "justcall:job:{job_id}:results"
CURSOR_KEY = "justcall:job:{job_id}:cursor"

RESULT_TTL = 86400
MAX_ERROR_COUNT = 50
DEFAULT_BATCH_DELAY_SEC = 1.0

SEND_FROM_NUMBER_KEY = "sendfromjustcallnumber"
SEND_FROM_NUMBER_ID_KEY = "sendfromjustcallnumberid"

# Aliases for backward compatibility with internal references
_clean_string = clean_string
_normalize_status = normalize_status
_normalize_header_key = normalize_header_key
_get_row_number = get_row_number
_should_skip_row = should_skip_row


def _normalize_number_key(value: Any) -> str:
    """JustCall-specific phone number normalization."""
    cleaned = clean_string(value)
    if not cleaned:
        return ""
    normalized, _, is_valid = normalize_phone_e164(cleaned)
    return normalized if is_valid and normalized else cleaned


def _parse_credentials(value: str) -> Tuple[str, str]:
    try:
        payload = json.loads(value or "{}")
    except json.JSONDecodeError as exc:
        raise JustCallAuthError("Invalid JustCall credential payload") from exc
    api_key = _clean_string(payload.get("api_key") or payload.get("apiKey"))
    api_secret = _clean_string(payload.get("api_secret") or payload.get("apiSecret"))
    if not api_key or not api_secret:
        raise JustCallAuthError("JustCall credentials missing api_key/api_secret")
    return api_key, api_secret


@dataclass
class RowContext:
    row_number: int
    phone: str
    contact: CampaignContact
    send_from_number: Optional[str] = None
    send_from_number_id: Optional[str] = None


class JustCallWorker:
    """Process JustCall push job asynchronously."""

    CHUNK_SIZE = 250

    def __init__(
        self,
        account_id: str,
        job_id: str,
        config: Dict[str, Any],
        rows: Optional[List[Dict[str, Any]]] = None,
        redis=None,
        sleep: Optional[callable] = None,
    ) -> None:
        self.account_id = account_id
        self.job_id = job_id
        self.config = JustCallConfig.model_validate(config).model_dump()
        self.rows = rows or []
        self.redis = redis
        self.sleep = sleep or asyncio.sleep
        self.summary = {
            "total_rows": len(self.rows),
            "processed_rows": 0,
            "enrolled": 0,
            "failed": 0,
            "skipped": 0,
            "invalid_phone": 0,
            "duplicate": 0,
            "messages_sent": 0,
            "messages_failed": 0,
        }
        self.errors: List[Dict[str, Any]] = []
        self.errors_truncated = False
        self._resume_cursor = 0
        self._header_map: Dict[str, str] = {}

    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, message: Optional[str] = None) -> 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)
        await self._update_job_meta(status=state, message=message)

    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)
        await self._update_job_meta(progress=progress, processed_rows=self.summary["processed_rows"])

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

    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)
        await self._update_job_meta(errors=payload)

    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)

    async def _update_job_meta(self, **updates: Any) -> None:
        redis = await self._get_redis()
        if not redis:
            return
        key = f"job:{self.job_id}"
        raw = await redis.get(key)
        meta: Dict[str, Any]
        if raw:
            try:
                meta = json.loads(raw)
            except Exception:
                meta = {}
        else:
            meta = {}
        meta.update({k: v for k, v in updates.items() if v is not None})
        await redis.set(key, json.dumps(meta), ex=RESULT_TTL)

    def _record_error(self, row_number: int, status: str, error: str) -> None:
        if len(self.errors) >= MAX_ERROR_COUNT:
            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["failed"]
            + self.summary["skipped"]
            + self.summary["invalid_phone"]
            + self.summary["duplicate"]
        )

    async def _load_resume_cursor(self) -> None:
        redis = await self._get_redis()
        if not redis:
            return
        raw = await redis.get(CURSOR_KEY.format(job_id=self.job_id))
        if raw is None:
            return
        try:
            value = int(raw)
            if value >= 0:
                self._resume_cursor = value
        except (TypeError, ValueError):
            return

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

    async def _load_rows(self) -> None:
        if self.rows:
            self.summary["total_rows"] = len(self.rows)
            return

        upload_id = self.config.get("upload_id")
        if not upload_id:
            return

        storage = get_storage_backend()
        if asyncio.iscoroutine(storage):
            storage = await storage
        content = await storage.read(upload_id)
        text = content.decode("utf-8-sig", errors="ignore")
        reader = csv.DictReader(io.StringIO(text))
        rows = list(reader)
        starting_row = int(self.config.get("starting_row") or 2)
        for idx, row in enumerate(rows):
            row["row_number"] = starting_row + idx
        self.rows = rows
        self.summary["total_rows"] = len(rows)

    async def _get_adapter(self) -> JustCallAdapter:
        profile_id = self.config.get("profile_id")
        if not profile_id:
            raise JustCallAuthError("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="justcall",
            )
        if not secret or not secret.value:
            raise JustCallAuthError("JustCall credentials not configured")
        api_key, api_secret = _parse_credentials(secret.value)
        return JustCallAdapter(api_key=api_key, api_secret=api_secret)

    def _build_contact(
        self,
        row: Dict[str, Any],
        mapping: Dict[str, Any],
        normalized_phone: str,
        tags: List[str],
    ) -> CampaignContact:
        def value_for(field: str) -> Optional[str]:
            column = mapping.get(field)
            if not column:
                return None
            value = row.get(column)
            text = _clean_string(value)
            return text if text else None

        custom_fields = {}
        for key, column in (mapping.get("custom_fields") or {}).items():
            if not column:
                continue
            raw = row.get(column)
            text = _clean_string(raw)
            if text:
                custom_fields[str(key)] = text

        if tags:
            custom_fields.setdefault("source", ", ".join(tags))

        return CampaignContact(
            phone=normalized_phone,
            first_name=value_for("first_name"),
            last_name=value_for("last_name"),
            email=value_for("email"),
            company=value_for("company"),
            custom_fields=custom_fields,
        )

    def _build_header_map(self, row: Dict[str, Any]) -> Dict[str, str]:
        header_map: Dict[str, str] = {}
        for key in row.keys():
            normalized = _normalize_header_key(key)
            if normalized and normalized not in header_map:
                header_map[normalized] = key
        return header_map

    def _extract_send_from(self, row: Dict[str, Any]) -> Tuple[Optional[str], Optional[str]]:
        if not self._header_map:
            self._header_map = self._build_header_map(row)
        number_key = self._header_map.get(SEND_FROM_NUMBER_KEY)
        number_id_key = self._header_map.get(SEND_FROM_NUMBER_ID_KEY)
        number = _clean_string(row.get(number_key)) if number_key else ""
        number_id = _clean_string(row.get(number_id_key)) if number_id_key else ""
        return (number or None, number_id or None)

    def _build_numbers_cache(self, numbers: List[Dict[str, Any]]) -> Dict[str, Dict[str, Any]]:
        by_number: Dict[str, Dict[str, Any]] = {}
        by_id: Dict[str, Dict[str, Any]] = {}
        for item in numbers:
            if not isinstance(item, dict):
                continue
            raw_number = _clean_string(item.get("number"))
            if not raw_number:
                continue
            number_key = _normalize_number_key(raw_number)
            entry = {
                "id": _clean_string(item.get("id")),
                "number": raw_number,
                "sms_enabled": item.get("sms_enabled"),
                "whatsapp_enabled": item.get("whatsapp_enabled"),
            }
            by_number[number_key] = entry
            by_number[raw_number] = entry
            if entry["id"]:
                by_id[entry["id"]] = entry
        return {"by_number": by_number, "by_id": by_id}

    def _lookup_number(
        self,
        cache: Optional[Dict[str, Dict[str, Any]]],
        number: Optional[str],
    ) -> Optional[Dict[str, Any]]:
        if not cache or not number:
            return None
        key = _normalize_number_key(number)
        by_number = cache.get("by_number") or {}
        return by_number.get(key) or by_number.get(number)

    def _lookup_number_by_id(
        self,
        cache: Optional[Dict[str, Dict[str, Any]]],
        number_id: Optional[str],
    ) -> Optional[Dict[str, Any]]:
        if not cache or not number_id:
            return None
        by_id = cache.get("by_id") or {}
        return by_id.get(number_id)

    def _capability_ok(self, entry: Optional[Dict[str, Any]], send_type: str) -> bool:
        if not entry:
            return True
        key = "sms_enabled" if send_type == "sms" else "whatsapp_enabled"
        value = entry.get(key)
        return True if value is None else bool(value)

    def _resolve_send_from_number(
        self,
        ctx: RowContext,
        settings: Dict[str, Any],
        numbers_cache: Optional[Dict[str, Dict[str, Any]]],
    ) -> Tuple[Optional[str], Optional[str]]:
        send_type = settings.get("type")
        default_number = _clean_string(settings.get("default_from_number"))

        candidate_number = _clean_string(ctx.send_from_number) if ctx.send_from_number else ""
        candidate_entry: Optional[Dict[str, Any]] = None
        if candidate_number:
            candidate_entry = self._lookup_number(numbers_cache, candidate_number)
        elif ctx.send_from_number_id:
            candidate_entry = self._lookup_number_by_id(numbers_cache, ctx.send_from_number_id)
            if candidate_entry:
                candidate_number = _clean_string(candidate_entry.get("number"))

        if not candidate_number:
            candidate_number = default_number
            candidate_entry = self._lookup_number(numbers_cache, candidate_number)

        if not candidate_number:
            return None, "JustCall number is required for messaging"

        if not self._capability_ok(candidate_entry, send_type):
            if default_number and default_number != candidate_number:
                fallback_entry = self._lookup_number(numbers_cache, default_number)
                if self._capability_ok(fallback_entry, send_type):
                    return default_number, None
            return None, (
                "Send-from number not SMS-enabled"
                if send_type == "sms"
                else "Send-from number not WhatsApp-enabled"
            )

        return candidate_number, None

    def _extract_error_key(self, error: Dict[str, Any]) -> Tuple[Optional[str], Optional[int], str]:
        phone = None
        row_index = None
        message = _clean_string(error.get("message") or error.get("error") or "Import failed")
        for key in ("phone", "contact", "number", "contact_number"):
            if error.get(key):
                phone = _clean_string(error.get(key))
                break
        for key in ("row_number", "row", "index"):
            if error.get(key) is None:
                continue
            try:
                row_index = int(error.get(key))
                break
            except (TypeError, ValueError):
                continue
        return phone, row_index, message

    def _resolve_message_settings(self) -> Optional[Dict[str, Any]]:
        send_message = _clean_string(self.config.get("send_message")).lower()
        if not send_message or send_message == "none":
            return None
        if send_message not in ("sms", "whatsapp"):
            raise JustCallError("Unsupported send_message option")

        justcall_number = _clean_string(self.config.get("justcall_number"))
        if not justcall_number:
            raise JustCallError("JustCall number is required for messaging")

        message_body = _clean_string(self.config.get("message_body"))
        template_id_raw = _clean_string(self.config.get("whatsapp_template_id"))
        template_id = int(template_id_raw) if template_id_raw.isdigit() else None

        if send_message == "sms":
            if not message_body:
                raise JustCallError("SMS message body is required")
            message_body = message_body[:160]

        if send_message == "whatsapp" and not (message_body or template_id):
            raise JustCallError("WhatsApp message body or template ID is required")

        return {
            "type": send_message,
            "default_from_number": justcall_number,
            "message_body": message_body,
            "template_id": template_id,
        }

    async def _send_message_with_retry(
        self,
        adapter: JustCallAdapter,
        *,
        settings: Dict[str, Any],
        to_number: str,
        from_number: str,
    ) -> Tuple[bool, Optional[str]]:
        async def _send_once() -> None:
            if settings["type"] == "sms":
                await adapter.send_sms(from_number, to_number, settings["message_body"])
            else:
                await adapter.send_whatsapp(
                    from_number,
                    to_number,
                    body=settings.get("message_body"),
                    template_id=settings.get("template_id"),
                )

        try:
            await _send_once()
            return True, None
        except JustCallRateLimitError:
            await self.sleep(2.0)
            try:
                await _send_once()
                return True, None
            except JustCallError as exc:
                return False, str(exc)
        except JustCallError as exc:
            return False, str(exc)
        except Exception as exc:
            return False, str(exc)

    async def _send_messages(
        self,
        adapter: JustCallAdapter,
        settings: Dict[str, Any],
        row_contexts: List[RowContext],
        payloads: List[Dict[str, Any]],
        numbers_cache: Optional[Dict[str, Dict[str, Any]]],
    ) -> None:
        payload_by_row = {payload.get("row_number"): payload for payload in payloads}
        for ctx in row_contexts:
            payload = payload_by_row.get(ctx.row_number)
            if not payload:
                continue
            from_number, resolve_error = self._resolve_send_from_number(
                ctx, settings, numbers_cache
            )
            if resolve_error:
                payload["message_sent"] = False
                payload["message_error"] = resolve_error
                self.summary["messages_failed"] += 1
                continue
            sent, error = await self._send_message_with_retry(
                adapter,
                settings=settings,
                to_number=ctx.phone,
                from_number=from_number,
            )
            timestamp = datetime.now(timezone.utc).isoformat()
            if sent:
                payload["message_sent"] = True
                payload["message_sent_at"] = timestamp
                self.summary["messages_sent"] += 1
            else:
                payload["message_sent"] = False
                payload["message_error"] = error or "Message failed"
                self.summary["messages_failed"] += 1
            await self.sleep(1.0)

    async def _build_batch_results(
        self,
        result: Dict[str, Any],
        row_contexts: List[RowContext],
        campaign_id: str,
    ) -> Tuple[List[Dict[str, Any]], List[RowContext]]:
        errors = result.get("errors") or []
        error_by_phone: Dict[str, str] = {}
        error_by_index: Dict[int, str] = {}
        for err in errors:
            if not isinstance(err, dict):
                continue
            phone, row_index, message = self._extract_error_key(err)
            if phone:
                normalized, _, _ = normalize_phone_e164(phone)
                if normalized:
                    error_by_phone[normalized] = message
            if row_index is not None:
                error_by_index[row_index] = message

        fallback_failures = result.get("failed_count", 0)
        if fallback_failures and not error_by_phone and not error_by_index:
            error_by_index = {idx: "JustCall bulk import reported failure" for idx in range(len(row_contexts))}

        raw_contacts = {}
        raw = result.get("raw")
        if isinstance(raw, dict):
            for key in ("contacts", "data", "results"):
                if isinstance(raw.get(key), list):
                    for item in raw[key]:
                        if not isinstance(item, dict):
                            continue
                        phone = _clean_string(item.get("phone") or item.get("number"))
                        contact_id = _clean_string(item.get("id") or item.get("contact_id"))
                        if phone and contact_id:
                            normalized, _, _ = normalize_phone_e164(phone)
                            if normalized:
                                raw_contacts[normalized] = contact_id

        timestamp = datetime.now(timezone.utc).isoformat()
        payloads: List[Dict[str, Any]] = []
        enrolled_contexts: List[RowContext] = []
        for idx, ctx in enumerate(row_contexts):
            error_msg = error_by_index.get(idx) or error_by_phone.get(ctx.phone)
            status = "failed" if error_msg else "enrolled"
            contact_id = raw_contacts.get(ctx.phone)
            result_payload = {
                "row_number": ctx.row_number,
                "status": status,
                "campaign_id": campaign_id,
                "contact_id": contact_id,
                "pushed_at": timestamp if status == "enrolled" else None,
                "error": error_msg,
            }
            payloads.append(result_payload)
            if status == "failed":
                self._record_error(ctx.row_number, status, error_msg or "Import failed")
            else:
                enrolled_contexts.append(ctx)
            self._increment_summary(status)
        return payloads, enrolled_contexts

    async def run(self) -> None:
        await self._load_resume_cursor()
        await self._load_rows()
        await self._set_state("processing", message="Starting JustCall push")
        await self._set_summary()
        await self._set_errors()

        mapping = self.config.get("mapping") or {}
        writeback = self.config.get("writeback") or {}
        campaign_id = _clean_string(self.config.get("campaign_id"))
        tags = self.config.get("tags") or []
        starting_row = int(self.config.get("starting_row") or 2)
        batch_delay = DEFAULT_BATCH_DELAY_SEC

        seen_phones: set[str] = set()
        row_contexts: List[RowContext] = []
        total_rows = len(self.rows)

        adapter: Optional[JustCallAdapter] = None
        message_settings = self._resolve_message_settings()
        numbers_cache: Optional[Dict[str, Dict[str, Any]]] = None
        try:
            adapter = await self._get_adapter()
            if message_settings:
                try:
                    numbers_cache = self._build_numbers_cache(await adapter.list_numbers())
                except JustCallError as exc:
                    logger.warning("[JUSTCALL] Failed to load number capabilities: %s", exc)
                    numbers_cache = {"by_number": {}, "by_id": {}}
            for idx, row in enumerate(self.rows[self._resume_cursor :], start=self._resume_cursor):
                row_number = _get_row_number(row, idx, starting_row)
                if _should_skip_row(row, campaign_id, writeback):
                    result_payload = {
                        "row_number": row_number,
                        "status": "skipped",
                        "campaign_id": campaign_id,
                        "error": "Already enrolled",
                    }
                    await self._store_result(result_payload)
                    self._increment_summary("skipped")
                    await self._set_resume_cursor(idx + 1)
                    continue

                phone_column = mapping.get("phone")
                if not phone_column:
                    result_payload = {
                        "row_number": row_number,
                        "status": "invalid_phone",
                        "campaign_id": campaign_id,
                        "error": "Phone mapping is required",
                    }
                    await self._store_result(result_payload)
                    self._record_error(row_number, "invalid_phone", "Phone mapping is required")
                    self._increment_summary("invalid_phone")
                    await self._set_resume_cursor(idx + 1)
                    continue

                raw_phone = _clean_string(row.get(phone_column))
                if not raw_phone:
                    result_payload = {
                        "row_number": row_number,
                        "status": "skipped",
                        "campaign_id": campaign_id,
                        "error": "Missing phone",
                    }
                    await self._store_result(result_payload)
                    self._increment_summary("skipped")
                    await self._set_resume_cursor(idx + 1)
                    continue

                normalized, _, is_valid = normalize_phone_e164(raw_phone)
                if not is_valid:
                    result_payload = {
                        "row_number": row_number,
                        "status": "invalid_phone",
                        "campaign_id": campaign_id,
                        "error": "Invalid phone format",
                    }
                    await self._store_result(result_payload)
                    self._record_error(row_number, "invalid_phone", "Invalid phone format")
                    self._increment_summary("invalid_phone")
                    await self._set_resume_cursor(idx + 1)
                    continue

                if normalized in seen_phones:
                    result_payload = {
                        "row_number": row_number,
                        "status": "duplicate",
                        "campaign_id": campaign_id,
                        "error": "Duplicate phone in selection",
                    }
                    await self._store_result(result_payload)
                    self._increment_summary("duplicate")
                    await self._set_resume_cursor(idx + 1)
                    continue

                seen_phones.add(normalized)
                contact = self._build_contact(row, mapping, normalized, tags)
                send_from_number = None
                send_from_number_id = None
                if message_settings:
                    send_from_number, send_from_number_id = self._extract_send_from(row)
                row_contexts.append(RowContext(
                    row_number=row_number,
                    phone=normalized,
                    contact=contact,
                    send_from_number=send_from_number,
                    send_from_number_id=send_from_number_id,
                ))

                if len(row_contexts) >= self.CHUNK_SIZE:
                    result = await adapter.bulk_import_contacts(campaign_id, [ctx.contact for ctx in row_contexts])
                    payloads, enrolled_contexts = await self._build_batch_results(
                        result, row_contexts, campaign_id
                    )
                    if message_settings and enrolled_contexts:
                        await self._send_messages(
                            adapter, message_settings, enrolled_contexts, payloads, numbers_cache
                        )
                    for payload in payloads:
                        await self._store_result(payload)
                    await self._set_summary()
                    await self._set_errors()
                    progress = int((self.summary["processed_rows"] / max(total_rows, 1)) * 100)
                    await self._set_progress(min(progress, 99))
                    row_contexts = []
                    await self._set_resume_cursor(idx + 1)
                    await self.sleep(batch_delay * random.uniform(0.5, 1.5))

            if row_contexts:
                result = await adapter.bulk_import_contacts(campaign_id, [ctx.contact for ctx in row_contexts])
                payloads, enrolled_contexts = await self._build_batch_results(
                    result, row_contexts, campaign_id
                )
                if message_settings and enrolled_contexts:
                    await self._send_messages(
                        adapter, message_settings, enrolled_contexts, payloads, numbers_cache
                    )
                for payload in payloads:
                    await self._store_result(payload)

            await self._set_summary()
            await self._set_errors()
            await self._set_progress(100)
            await self._set_state("completed", message="JustCall push completed")
            await self._send_completion_email()
        except (JustCallAuthError, JustCallRateLimitError, JustCallError) as exc:
            await self._set_state("failed", message=str(exc))
            await self._set_progress(100)
            raise
        except Exception as exc:
            await self._set_state("failed", message=str(exc))
            await self._set_progress(100)
            raise
        finally:
            if adapter is not None:
                await adapter.close()

    async def _send_completion_email(self) -> None:
        notify_email = _clean_string(self.config.get("notify_email"))
        if not notify_email:
            return
        await send_justcall_complete_notification(
            user_email=notify_email,
            job_name="JustCall Autodialer Push",
            job_id=self.job_id,
            total_rows=int(self.summary.get("total_rows", 0)),
            enrolled_count=int(self.summary.get("enrolled", 0)),
            failed_count=int(self.summary.get("failed", 0)),
            skipped_count=int(
                self.summary.get("skipped", 0)
                + self.summary.get("invalid_phone", 0)
                + self.summary.get("duplicate", 0)
            ),
            spreadsheet_url=self.config.get("spreadsheet_url"),
        )
