"""Sequencer export API for Smartlead + Instantly + Apollo + Amplemarket (Sheets add-on)."""

from __future__ import annotations

import asyncio
import hashlib
import json
import logging
import re
from datetime import datetime, timezone
from typing import Any, Dict, List, Optional, Literal

from fastapi import APIRouter, Depends, HTTPException, Query, Security
from fastapi.security import HTTPAuthorizationCredentials
from pydantic import BaseModel, Field, field_validator, model_validator
from sqlalchemy.ext.asyncio import AsyncSession

from ...auth_core import OrgContext, get_current_identity
from ...auth_security import api_key_hdr, http_bearer, require_account
from ...db import get_session
from ...middleware.quota_checker import TIER_LIMITS, get_quota
from ...services.secret_storage import get_saas_secret_backend
from ..progress import get_redis
from ...workers.sequencer_worker import (
    ERRORS_KEY,
    PROGRESS_KEY,
    RESULTS_KEY,
    SUMMARY_KEY,
    STATE_KEY,
    SequencerExportWorker,
)
from ...workers.amplemarket_sequencer_worker import AmplemarketSequencerWorker
from g_gremlin.sequencer import (
    SequencerAuthError,
    SequencerError,
    SequencerRateLimitError,
    get_provider,
)

logger = logging.getLogger(__name__)

router = APIRouter(prefix="/api/v2/sequencer", tags=["sequencer"])

META_KEY = "sequencer:job:{job_id}:meta"
IDEMPOTENCY_KEY = "sequencer:job:{account_id}:{idempotency_key}"
RESULT_TTL = 86400
MAX_ERRORS = 50
EMAIL_RE = re.compile(r"^[^@\s]+@[^@\s]+\.[^@\s]+$")
SUPPORTED_PROVIDERS = {"smartlead", "instantly", "apollo", "amplemarket"}
PROVIDER_LABELS = {
    "smartlead": "Smartlead",
    "instantly": "Instantly",
    "apollo": "Apollo",
    "amplemarket": "Amplemarket",
}
PROVIDER_REQUIRED_FIELDS = {
    "smartlead": ["email"],
    "instantly": ["email"],
    "apollo": ["email"],
    "amplemarket": [],
}

SequencerProviderName = Literal["smartlead", "instantly", "apollo", "amplemarket"]


class SourceInfo(BaseModel):
    spreadsheet_id: str
    sheet_name: str
    range_a1: str


class SequencerMapping(BaseModel):
    email: Optional[str] = None
    first_name: Optional[str] = None
    last_name: Optional[str] = None
    company: Optional[str] = None
    title: Optional[str] = None
    phone: Optional[str] = None
    linkedin_url: Optional[str] = None
    domain: Optional[str] = None
    send_email_from_email_account_id: Optional[str] = None
    custom_vars: Dict[str, str] = Field(default_factory=dict)


class SequencerWriteback(BaseModel):
    enabled: bool = True
    status_column: str = "Sequencer Status"
    campaign_column: str = "Sequencer Campaign"
    lead_id_column: str = "Sequencer Lead ID"
    pushed_at_column: str = "Sequencer Pushed At"
    error_column: str = "Sequencer Error"


class SequencerConfig(BaseModel):
    provider: SequencerProviderName = "smartlead"
    profile_id: str
    campaign_id: str
    send_email_from_email_account_id: Optional[str] = None
    source: SourceInfo
    mapping: SequencerMapping
    writeback: SequencerWriteback = Field(default_factory=SequencerWriteback)

    @field_validator("provider", mode="before")
    @classmethod
    def normalize_provider(cls, value: Any) -> Any:
        return _normalize_provider(value)

    @model_validator(mode="after")
    def validate_apollo_account(self) -> "SequencerConfig":
        if self.provider == "apollo":
            account_id = (self.send_email_from_email_account_id or "").strip()
            mapping_id = (self.mapping.send_email_from_email_account_id or "").strip()
            if not account_id and not mapping_id:
                raise ValueError("send_email_from_email_account_id is required for Apollo")
        return self


class SequencerRowsRequest(BaseModel):
    config: SequencerConfig
    rows: List[Dict[str, Any]] = Field(default_factory=list)


class CampaignsRequest(BaseModel):
    provider: SequencerProviderName = "smartlead"
    profile_id: str

    @field_validator("provider", mode="before")
    @classmethod
    def normalize_provider(cls, value: Any) -> Any:
        return _normalize_provider(value)


class ProfileInfo(BaseModel):
    id: str
    provider: str
    label: str
    key_suffix: Optional[str] = None
    connected_at: Optional[str] = None


class ProviderStatus(BaseModel):
    configured: bool


class ProfilesResponse(BaseModel):
    profiles: List[ProfileInfo]
    providers: Optional[Dict[str, ProviderStatus]] = None


class CampaignInfo(BaseModel):
    id: str
    name: str
    status: Optional[str] = None


class CampaignsResponse(BaseModel):
    campaigns: List[CampaignInfo]


class SendersRequest(BaseModel):
    provider: SequencerProviderName = "smartlead"
    profile_id: str


class SenderAccount(BaseModel):
    email_account_id: str
    email: Optional[str] = None
    name: Optional[str] = None
    owner_id: Optional[str] = None
    owner_email: Optional[str] = None
    status: Optional[str] = None


class SendersResponse(BaseModel):
    accounts: List[SenderAccount]


class PreviewError(BaseModel):
    row_number: int
    field: str
    message: str


class PreviewResponse(BaseModel):
    total_rows: int
    valid_rows: int
    invalid_rows: int
    skipped_rows: int
    errors: List[PreviewError]


class JobStartResponse(BaseModel):
    job_id: str
    status: str
    started_new_job: bool = True


class JobSummary(BaseModel):
    total_rows: int = 0
    processed_rows: int = 0
    enrolled: int = 0
    exists: int = 0
    skipped: int = 0
    failed: int = 0


class JobError(BaseModel):
    row_number: int
    status: str
    error: str


class JobStatusResponse(BaseModel):
    job_id: str
    status: str
    progress: int
    summary: JobSummary
    errors: List[JobError]
    errors_truncated: bool = False


class JobResultsResponse(BaseModel):
    job_id: str
    cursor: int
    next_cursor: Optional[int] = None
    total: int
    results: List[Dict[str, Any]]


async def require_sequencer_context(
    bearer: Optional[HTTPAuthorizationCredentials] = Security(http_bearer),
    api_key: Optional[str] = Security(api_key_hdr),
    db: AsyncSession = Depends(get_session),
) -> OrgContext:
    account_id = await require_account(bearer=bearer, api_key=api_key, db=db)
    user_id = "api-key"
    email = None
    if bearer:
        try:
            ident = await get_current_identity(bearer)
            user_id = ident.get("user_id") or user_id
            email = ident.get("email")
        except HTTPException:
            pass
    return OrgContext(account_id=account_id, user_id=user_id, email=email)


async def require_unleashed_tier(org: OrgContext, db: AsyncSession) -> None:
    quota = await get_quota(org.account_id, db)
    tier = quota.tier.lower()
    limits = TIER_LIMITS.get(tier, TIER_LIMITS.get("free", {}))
    if not limits.get("byo_enrichment_enabled", False):
        raise HTTPException(
            status_code=403,
            detail={
                "error": "TIER_REQUIRED",
                "message": "Requires Unleashed tier",
                "current_tier": tier,
                "required_tier": "unleashed",
                "upgrade_url": "/pricing",
            },
        )


def _normalize_provider(value: Optional[str]) -> str:
    return (value or "smartlead").strip().lower()


def _provider_label(name: str) -> str:
    return PROVIDER_LABELS.get(name, name)


def _validate_provider(name: Optional[str]) -> str:
    provider = _normalize_provider(name)
    if provider not in SUPPORTED_PROVIDERS:
        raise HTTPException(
            status_code=400,
            detail="Provider not supported. Valid: smartlead, instantly, apollo, amplemarket",
        )
    return provider


def _mapping_hash(mapping: SequencerMapping) -> str:
    payload = mapping.model_dump()
    return hashlib.sha256(json.dumps(payload, sort_keys=True).encode("utf-8")).hexdigest()


def _idempotency_key(config: SequencerConfig) -> str:
    mapping_hash = _mapping_hash(config.mapping)
    send_from_id = (config.send_email_from_email_account_id or "").strip()
    raw = "|".join([
        _normalize_provider(config.provider),
        config.source.spreadsheet_id,
        config.source.sheet_name,
        config.source.range_a1,
        config.campaign_id,
        send_from_id,
        mapping_hash,
    ])
    return hashlib.sha256(raw.encode("utf-8")).hexdigest()


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 = str(value or "").strip()
    if not text:
        return False
    return bool(EMAIL_RE.match(text))


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


def _get_secret_for_profile(
    secrets: List[Dict[str, Any]],
    profile_id: str,
) -> Optional[Dict[str, Any]]:
    for secret in secrets:
        if secret.get("id") == profile_id:
            return secret
    return None


def _normalize_sender(provider: str, account: Dict[str, Any]) -> SenderAccount:
    user = account.get("user") or {}
    email_account_id = (
        account.get("id")
        or account.get("email_account_id")
        or account.get("account_id")
        or ""
    )
    owner_id = account.get("user_id") or user.get("id")
    owner_email = account.get("user_email") or user.get("email")
    name = account.get("name") or account.get("display_name") or account.get("sender_name")

    if provider in {"smartlead", "instantly"}:
        return SenderAccount(
            email_account_id=str(email_account_id),
            email=account.get("email") or account.get("email_address"),
            name=name,
            owner_id=None,
            owner_email=None,
            status=account.get("status") or account.get("state"),
        )

    return SenderAccount(
        email_account_id=str(email_account_id),
        email=account.get("email") or account.get("email_address"),
        name=name,
        owner_id=str(owner_id) if owner_id else None,
        owner_email=owner_email,
        status=account.get("status") or account.get("state"),
    )


@router.get("/profiles", response_model=ProfilesResponse)
async def list_profiles(
    provider: str = Query("smartlead"),
    org: OrgContext = Depends(require_sequencer_context),
    db: AsyncSession = Depends(get_session),
) -> ProfilesResponse:
    await require_unleashed_tier(org, db)

    provider_name = _validate_provider(provider)
    provider_label = _provider_label(provider_name)
    backend = get_saas_secret_backend()
    all_secrets = await backend.list_secrets(db, org.account_id, integration=None)

    provider_status: Dict[str, ProviderStatus] = {}
    for pname in ("smartlead", "instantly", "apollo", "amplemarket"):
        # Apollo uses OAuth access_token, others use api_key
        expected_key = "access_token" if pname == "apollo" else "api_key"
        pname_secrets = [
            s for s in all_secrets
            if s.get("integration") == pname and s.get("key_name") == expected_key
        ]
        provider_status[pname] = ProviderStatus(configured=bool(pname_secrets))

    # Apollo uses OAuth access_token, others use api_key
    expected_key_name = "access_token" if provider_name == "apollo" else "api_key"
    selected_secrets = [
        s for s in all_secrets
        if s.get("integration") == provider_name and s.get("key_name") == expected_key_name
    ]

    profiles = []
    for secret in selected_secrets:
        key_suffix = secret.get("key_suffix")
        label = provider_label + (f" (****{key_suffix})" if key_suffix else "")
        profiles.append(ProfileInfo(
            id=str(secret.get("id")),
            provider=provider_name,
            label=label,
            key_suffix=key_suffix,
            connected_at=secret.get("created_at"),
        ))

    return ProfilesResponse(profiles=profiles, providers=provider_status)


@router.post("/campaigns", response_model=CampaignsResponse)
async def list_campaigns(
    request: CampaignsRequest,
    org: OrgContext = Depends(require_sequencer_context),
    db: AsyncSession = Depends(get_session),
) -> CampaignsResponse:
    await require_unleashed_tier(org, db)

    provider_name = _validate_provider(request.provider)
    provider_label = _provider_label(provider_name)

    backend = get_saas_secret_backend()
    secrets = await backend.list_secrets(db, org.account_id, integration=provider_name)
    # Apollo uses OAuth access_token, others use api_key
    expected_key = "access_token" if provider_name == "apollo" else "api_key"
    api_secrets = [s for s in secrets if s.get("key_name") == expected_key]
    if not api_secrets:
        raise HTTPException(status_code=404, detail=f"{provider_label} credentials not configured")

    profile = _get_secret_for_profile(api_secrets, request.profile_id)
    if not profile:
        raise HTTPException(status_code=404, detail="Profile not found")
    secret = await backend.get_secret_by_id(
        db=db,
        secret_id=request.profile_id,
        workspace_id=org.account_id,
        integration=provider_name,
    )
    if not secret or not secret.value:
        raise HTTPException(status_code=404, detail=f"{provider_label} credentials not configured")

    if provider_name == "amplemarket":
        from g_gremlin.amplemarket import (
            AmplemarketAuthError,
            AmplemarketClient,
            AmplemarketError,
            AmplemarketRateLimitError,
        )

        client = AmplemarketClient(api_key=str(secret.value))
        try:
            sequences = await asyncio.to_thread(client.list_sequences)
        except AmplemarketAuthError as exc:
            logger.warning("Amplemarket auth error: %s", exc)
            raise HTTPException(status_code=401, detail="Authentication failed")
        except AmplemarketRateLimitError as exc:
            logger.warning("Amplemarket rate limit: %s", exc)
            raise HTTPException(status_code=429, detail=str(exc))
        except AmplemarketError as exc:
            logger.error("Amplemarket error: %s", exc)
            raise HTTPException(status_code=502, detail=str(exc))
        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)

        items = [
            CampaignInfo(
                id=s.id,
                name=s.name,
                status=str(s.status) if getattr(s, "status", None) is not None else None,
            )
            for s in sequences
        ]
        return CampaignsResponse(campaigns=items)

    # Apollo OAuth uses Bearer auth, API keys use x-api-key header
    use_bearer = provider_name == "apollo"
    provider = get_provider(provider_name, api_key=secret.value, use_bearer_auth=use_bearer)
    try:
        campaigns = await asyncio.to_thread(provider.list_campaigns)
    except SequencerAuthError as exc:
        logger.warning("Sequencer auth error for %s: %s", provider_name, exc)
        raise HTTPException(status_code=401, detail="Authentication failed")
    except SequencerRateLimitError as exc:
        logger.warning("Sequencer rate limit for %s: %s", provider_name, exc)
        raise HTTPException(status_code=429, detail=str(exc))
    except SequencerError as exc:
        logger.error("Sequencer error for %s: %s", provider_name, exc)
        raise HTTPException(status_code=502, detail=str(exc))
    finally:
        provider.close()

    items = [
        CampaignInfo(
            id=c.id,
            name=c.name,
            status=str(c.status) if c.status is not None else None,
        )
        for c in campaigns
    ]
    return CampaignsResponse(campaigns=items)


@router.post("/senders", response_model=SendersResponse)
async def list_senders(
    request: SendersRequest,
    org: OrgContext = Depends(require_sequencer_context),
    db: AsyncSession = Depends(get_session),
) -> SendersResponse:
    await require_unleashed_tier(org, db)

    provider_name = _validate_provider(request.provider)
    provider_label = _provider_label(provider_name)

    backend = get_saas_secret_backend()
    secrets = await backend.list_secrets(db, org.account_id, integration=provider_name)
    expected_key = "access_token" if provider_name == "apollo" else "api_key"
    api_secrets = [s for s in secrets if s.get("key_name") == expected_key]
    if not api_secrets:
        raise HTTPException(status_code=404, detail=f"{provider_label} credentials not configured")

    profile = _get_secret_for_profile(api_secrets, request.profile_id)
    if not profile:
        raise HTTPException(status_code=404, detail="Profile not found")
    secret = await backend.get_secret_by_id(
        db=db,
        secret_id=request.profile_id,
        workspace_id=org.account_id,
        integration=provider_name,
    )
    if not secret or not secret.value:
        raise HTTPException(status_code=404, detail=f"{provider_label} credentials not configured")

    if provider_name == "amplemarket":
        return SendersResponse(accounts=[])

    use_bearer = provider_name == "apollo"
    provider = get_provider(provider_name, api_key=secret.value, use_bearer_auth=use_bearer)
    try:
        accounts = await asyncio.to_thread(provider.list_email_accounts)
    except SequencerAuthError as exc:
        logger.warning("Sequencer auth error for %s: %s", provider_name, exc)
        raise HTTPException(status_code=401, detail="Authentication failed")
    except SequencerRateLimitError as exc:
        logger.warning("Sequencer rate limit for %s: %s", provider_name, exc)
        raise HTTPException(status_code=429, detail=str(exc))
    except SequencerError as exc:
        if "404" in str(exc):
            return SendersResponse(accounts=[])
        logger.error("Sequencer sender list error for %s: %s", provider_name, exc)
        raise HTTPException(status_code=502, detail=str(exc))
    finally:
        provider.close()

    normalized = [
        _normalize_sender(provider_name, account)
        for account in (accounts or [])
        if isinstance(account, dict)
    ]
    return SendersResponse(accounts=normalized)


@router.post("/jobs/preview", response_model=PreviewResponse)
async def preview_job(
    request: SequencerRowsRequest,
    org: OrgContext = Depends(require_sequencer_context),
    db: AsyncSession = Depends(get_session),
) -> PreviewResponse:
    await require_unleashed_tier(org, db)

    config = request.config
    provider_name = _validate_provider(config.provider)
    provider_label = _provider_label(provider_name)

    backend = get_saas_secret_backend()
    secrets = await backend.list_secrets(db, org.account_id, integration=provider_name)
    # Apollo uses OAuth access_token, others use api_key
    expected_key = "access_token" if provider_name == "apollo" else "api_key"
    api_secrets = [s for s in secrets if s.get("key_name") == expected_key]
    if not api_secrets:
        raise HTTPException(status_code=404, detail=f"{provider_label} credentials not configured")
    if not _get_secret_for_profile(api_secrets, config.profile_id):
        raise HTTPException(status_code=404, detail="Profile not found")
    if not config.campaign_id:
        raise HTTPException(status_code=400, detail="campaign_id is required")
    apollo_default_send_from = (config.send_email_from_email_account_id or "").strip()
    apollo_sender_column = config.mapping.send_email_from_email_account_id
    if provider_name == "apollo" and not apollo_default_send_from and not apollo_sender_column:
        raise HTTPException(
            status_code=400,
            detail="send_email_from_email_account_id is required for Apollo",
        )

    required_fields = PROVIDER_REQUIRED_FIELDS.get(provider_name, ["email"])
    if provider_name == "amplemarket":
        email_column = config.mapping.email
        linkedin_column = config.mapping.linkedin_url
        if not email_column and not linkedin_column:
            raise HTTPException(
                status_code=400,
                detail="Email or LinkedIn URL mapping is required for Amplemarket",
            )
    total_rows = len(request.rows)
    valid_rows = 0
    invalid_rows = 0
    skipped_rows = 0
    errors: List[PreviewError] = []

    for idx, row in enumerate(request.rows):
        row_number = _get_row_number(row, idx)
        if _should_skip_row(row, config.campaign_id, config.writeback):
            skipped_rows += 1
            continue

        if provider_name == "amplemarket":
            email_column = config.mapping.email
            linkedin_column = config.mapping.linkedin_url
            email_value = row.get(email_column) if email_column else None
            linkedin_value = row.get(linkedin_column) if linkedin_column else None
            email_text = str(email_value or "").strip()
            linkedin_text = str(linkedin_value or "").strip()

            if not email_text and not linkedin_text:
                invalid_rows += 1
                if len(errors) < MAX_ERRORS:
                    errors.append(PreviewError(
                        row_number=row_number,
                        field="email_or_linkedin_url",
                        message="Missing required field",
                    ))
                continue

            if email_text and not _is_valid_email(email_text) and not linkedin_text:
                invalid_rows += 1
                if len(errors) < MAX_ERRORS:
                    errors.append(PreviewError(
                        row_number=row_number,
                        field="email",
                        message="Invalid email format",
                    ))
                continue

            valid_rows += 1
            continue

        if "email" in required_fields:
            email_column = config.mapping.email
            email_value = row.get(email_column) if email_column else None
            if not email_column:
                invalid_rows += 1
                if len(errors) < MAX_ERRORS:
                    errors.append(PreviewError(
                        row_number=row_number,
                        field="email",
                        message="Email mapping is required",
                    ))
                continue

            if not email_value:
                invalid_rows += 1
                if len(errors) < MAX_ERRORS:
                    errors.append(PreviewError(
                        row_number=row_number,
                        field="email",
                        message="Missing required field",
                    ))
                continue

            if not _is_valid_email(email_value):
                invalid_rows += 1
                if len(errors) < MAX_ERRORS:
                    errors.append(PreviewError(
                        row_number=row_number,
                        field="email",
                        message="Invalid email format",
                    ))
                continue

        if provider_name == "apollo" and not apollo_default_send_from:
            sender_value = row.get(apollo_sender_column) if apollo_sender_column else None
            if not sender_value:
                invalid_rows += 1
                if len(errors) < MAX_ERRORS:
                    errors.append(PreviewError(
                        row_number=row_number,
                        field="send_email_from_email_account_id",
                        message="Missing required field",
                    ))
                continue

        valid_rows += 1

    return PreviewResponse(
        total_rows=total_rows,
        valid_rows=valid_rows,
        invalid_rows=invalid_rows,
        skipped_rows=skipped_rows,
        errors=errors,
    )


@router.post("/jobs", response_model=JobStartResponse)
async def submit_job(
    request: SequencerRowsRequest,
    org: OrgContext = Depends(require_sequencer_context),
    db: AsyncSession = Depends(get_session),
) -> JobStartResponse:
    await require_unleashed_tier(org, db)

    config = request.config
    provider_name = _validate_provider(config.provider)
    provider_label = _provider_label(provider_name)

    backend = get_saas_secret_backend()
    secrets = await backend.list_secrets(db, org.account_id, integration=provider_name)
    # Apollo uses OAuth access_token, others use api_key
    expected_key = "access_token" if provider_name == "apollo" else "api_key"
    api_secrets = [s for s in secrets if s.get("key_name") == expected_key]
    if not api_secrets:
        raise HTTPException(status_code=404, detail=f"{provider_label} credentials not configured")
    if not _get_secret_for_profile(api_secrets, config.profile_id):
        raise HTTPException(status_code=404, detail="Profile not found")
    if not config.campaign_id:
        raise HTTPException(status_code=400, detail="campaign_id is required")
    if not request.rows:
        raise HTTPException(status_code=400, detail="No rows submitted")

    if provider_name == "amplemarket":
        if not config.mapping.email and not config.mapping.linkedin_url:
            raise HTTPException(
                status_code=400,
                detail="Email or LinkedIn URL mapping is required for Amplemarket",
            )

    redis = await get_redis()
    idem_key = _idempotency_key(config)
    existing = await redis.get(IDEMPOTENCY_KEY.format(account_id=org.account_id, idempotency_key=idem_key))
    if existing:
        job_id = existing.decode() if isinstance(existing, (bytes, bytearray)) else str(existing)
        return JobStartResponse(job_id=job_id, status="queued", started_new_job=False)

    job_id = hashlib.sha256(f"{idem_key}:{datetime.now(timezone.utc).isoformat()}".encode("utf-8")).hexdigest()[:24]

    await redis.setex(
        IDEMPOTENCY_KEY.format(account_id=org.account_id, idempotency_key=idem_key),
        RESULT_TTL,
        job_id,
    )

    meta = {
        "account_id": org.account_id,
        "created_at": datetime.now(timezone.utc).isoformat(),
        "total_rows": len(request.rows),
        "config": config.model_dump(),
    }
    await redis.setex(META_KEY.format(job_id=job_id), RESULT_TTL, json.dumps(meta))
    await redis.setex(STATE_KEY.format(job_id=job_id), RESULT_TTL, "queued")
    await redis.setex(PROGRESS_KEY.format(job_id=job_id), RESULT_TTL, "0")
    await redis.setex(SUMMARY_KEY.format(job_id=job_id), RESULT_TTL, json.dumps({}))
    await redis.setex(
        ERRORS_KEY.format(job_id=job_id),
        RESULT_TTL,
        json.dumps({"errors": [], "errors_truncated": False}),
    )

    worker_config = config.model_dump()
    if provider_name == "amplemarket":
        worker = AmplemarketSequencerWorker(
            account_id=org.account_id,
            job_id=job_id,
            config=worker_config,
            rows=request.rows,
            redis=redis,
        )
    else:
        worker = SequencerExportWorker(
            account_id=org.account_id,
            job_id=job_id,
            config=worker_config,
            rows=request.rows,
            redis=redis,
        )
    asyncio.create_task(worker.run())

    return JobStartResponse(job_id=job_id, status="queued", started_new_job=True)


@router.get("/jobs/{job_id}", response_model=JobStatusResponse)
async def get_job_status(
    job_id: str,
    org: OrgContext = Depends(require_sequencer_context),
) -> JobStatusResponse:
    redis = await get_redis()
    meta_raw = await redis.get(META_KEY.format(job_id=job_id))
    if not meta_raw:
        raise HTTPException(status_code=404, detail="Job not found")

    meta_text = meta_raw.decode() if isinstance(meta_raw, (bytes, bytearray)) else meta_raw
    meta = json.loads(meta_text)
    if meta.get("account_id") != org.account_id:
        raise HTTPException(status_code=404, detail="Job not found")

    state = await redis.get(STATE_KEY.format(job_id=job_id))
    progress = await redis.get(PROGRESS_KEY.format(job_id=job_id))
    summary_raw = await redis.get(SUMMARY_KEY.format(job_id=job_id))
    errors_raw = await redis.get(ERRORS_KEY.format(job_id=job_id))

    state_val = state.decode() if isinstance(state, (bytes, bytearray)) else (state or "unknown")
    progress_val = int(progress) if progress else 0

    summary_data = {}
    if summary_raw:
        summary_text = summary_raw.decode() if isinstance(summary_raw, (bytes, bytearray)) else summary_raw
        try:
            summary_data = json.loads(summary_text)
        except Exception:
            summary_data = {}

    error_data = {"errors": [], "errors_truncated": False}
    if errors_raw:
        errors_text = errors_raw.decode() if isinstance(errors_raw, (bytes, bytearray)) else errors_raw
        try:
            error_data = json.loads(errors_text)
        except Exception:
            error_data = {"errors": [], "errors_truncated": False}

    return JobStatusResponse(
        job_id=job_id,
        status=state_val,
        progress=progress_val,
        summary=JobSummary(**summary_data),
        errors=[JobError(**err) for err in error_data.get("errors", [])],
        errors_truncated=bool(error_data.get("errors_truncated")),
    )


@router.get("/jobs/{job_id}/results", response_model=JobResultsResponse)
async def get_job_results(
    job_id: str,
    cursor: int = Query(0, ge=0),
    limit: int = Query(500, ge=1, le=2000),
    org: OrgContext = Depends(require_sequencer_context),
) -> JobResultsResponse:
    redis = await get_redis()
    meta_raw = await redis.get(META_KEY.format(job_id=job_id))
    if not meta_raw:
        raise HTTPException(status_code=404, detail="Job not found")

    meta_text = meta_raw.decode() if isinstance(meta_raw, (bytes, bytearray)) else meta_raw
    meta = json.loads(meta_text)
    if meta.get("account_id") != org.account_id:
        raise HTTPException(status_code=404, detail="Job not found")

    results_key = RESULTS_KEY.format(job_id=job_id)
    total = await redis.llen(results_key)
    if total == 0:
        return JobResultsResponse(job_id=job_id, cursor=cursor, total=0, results=[])

    end = min(total, cursor + limit) - 1
    raw_results = await redis.lrange(results_key, cursor, end)

    results = []
    for item in raw_results:
        text = item.decode() if isinstance(item, (bytes, bytearray)) else item
        try:
            results.append(json.loads(text))
        except Exception:
            continue

    next_cursor = cursor + len(results)
    if next_cursor >= total:
        next_cursor = None

    return JobResultsResponse(
        job_id=job_id,
        cursor=cursor,
        next_cursor=next_cursor,
        total=total,
        results=results,
    )
