"""Contract issuance service — orchestrates the full issue flow for INDL-28."""
from __future__ import annotations

import logging
import secrets
from datetime import datetime, timezone
from typing import Optional
from uuid import UUID

from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession

from src.core.config import settings
from src.core.exceptions import ConflictError, NotFoundError
from src.apps.sales.models.contract import Contract
from src.apps.sales.models.opportunity import Opportunity
from src.apps.billing.services.invoice_service import InvoiceService
from src.apps.sales.schemas.responses import ContractIssueResponse, IssuedInvoiceInfo

logger = logging.getLogger(__name__)


def generate_payment_token() -> str:
    """Generate a 48-byte URL-safe token encoded as 64-char hex string."""
    return secrets.token_hex(32)


class ContractIssueService:

    @staticmethod
    async def issue(
        db: AsyncSession,
        tenant_id: str,
        contract_id: UUID,
        current_user,
        arq_redis=None,
    ) -> ContractIssueResponse:
        """Orchestrate the full contract issuance flow:

        1. Load and validate the contract (not already issued, signed).
        2. Generate payment token and URL.
        3. Create invoice(s) based on payment_plan_type.
        4. Update contract issuance fields.
        5. Advance linked opportunity to 'contract_sent'.
        6. Enqueue send_contract_email ARQ job.
        7. Return ContractIssueResponse.
        """
        # 1. Load contract with row lock to prevent concurrent issue
        locked = await db.execute(
            select(Contract)
            .where(
                Contract.id == contract_id,
                Contract.tenant_id == tenant_id,
                Contract.deleted_at.is_(None),
            )
            .with_for_update()
        )
        contract = locked.scalar_one_or_none()
        if not contract:
            raise NotFoundError("Contract not found.")

        # Double-issue guard
        if contract.contract_sent_at is not None:
            raise ConflictError("Contract has already been issued.")

        # 2. Generate payment token and URL
        token = generate_payment_token()
        app_domain = getattr(settings, "APP_DOMAIN", "indelis.com")

        # Resolve tenant subdomain for the payment URL
        payment_url = await ContractIssueService._build_payment_url(
            db, tenant_id, token, app_domain
        )

        # 3. Create invoices based on payment plan
        payment_plan = contract.payment_plan_type or "full"
        created_invoices = await InvoiceService.create_contract_invoices(
            db=db,
            tenant_id=tenant_id,
            contract_id=contract.id,
            payment_plan_type=payment_plan,
            total_amount=contract.total_amount,
            purchaser_name=contract.purchaser_name,
            purchaser_email=contract.purchaser_email,
        )

        # 4. Update contract issuance fields
        contract.payment_token = token
        contract.payment_link_url = payment_url
        contract.contract_sent_at = datetime.now(timezone.utc)
        contract.contract_email_sent_to = contract.purchaser_email
        await db.flush()

        # 5. Advance opportunity stage to contract_sent
        stage_updated = await ContractIssueService._advance_opportunity_stage(
            db, tenant_id, contract.opportunity_id
        )

        # 6. Enqueue SES email ARQ job (fire-and-forget)
        if arq_redis:
            await ContractIssueService._enqueue_contract_email(
                tenant_id=tenant_id,
                contract_id=contract.id,
                payment_link_url=payment_url,
                arq_redis=arq_redis,
            )

        # 7. Build response with invoice details
        invoice_infos = []
        for idx, inv in enumerate(created_invoices):
            if payment_plan == "deposit_50":
                label = "Deposit (50%)" if idx == 0 else "Balance on interment"
            else:
                label = "Full payment"
            invoice_infos.append(
                IssuedInvoiceInfo(
                    id=inv.id,
                    amount=inv.total_amount,
                    due_date=inv.due_date.isoformat() if inv.due_date else None,
                    label=label,
                )
            )

        return ContractIssueResponse(
            contract_id=contract.id,
            contract_number=contract.contract_number,
            email_sent_to=contract.purchaser_email,
            payment_link_url=payment_url,
            invoices_created=invoice_infos,
            opportunity_stage_updated_to="contract_sent" if stage_updated else None,
        )

    @staticmethod
    async def resend(
        db: AsyncSession,
        tenant_id: str,
        contract_id: UUID,
        current_user,
        arq_redis=None,
    ) -> ContractIssueResponse:
        """Re-enqueue the contract email for an already-issued contract.

        Reuses the existing ``payment_token``/``payment_link_url`` unchanged —
        no invoice creation, no token regeneration, no opportunity-stage change.
        """
        locked = await db.execute(
            select(Contract)
            .where(
                Contract.id == contract_id,
                Contract.tenant_id == tenant_id,
                Contract.deleted_at.is_(None),
            )
            .with_for_update()
        )
        contract = locked.scalar_one_or_none()
        if not contract:
            raise NotFoundError("Contract not found.")

        if contract.contract_sent_at is None:
            raise ConflictError("Contract has not been issued yet — nothing to resend.")

        if arq_redis:
            await ContractIssueService._enqueue_contract_email(
                tenant_id=tenant_id,
                contract_id=contract.id,
                payment_link_url=contract.payment_link_url,
                arq_redis=arq_redis,
            )
            logger.info(
                "[resend] Contract %s email resent by user %s.", contract.id, current_user.id
            )

        return ContractIssueResponse(
            contract_id=contract.id,
            contract_number=contract.contract_number,
            email_sent_to=contract.contract_email_sent_to,
            payment_link_url=contract.payment_link_url,
            invoices_created=[],
            opportunity_stage_updated_to=None,
        )

    @staticmethod
    async def _enqueue_contract_email(
        tenant_id: str, contract_id: UUID, payment_link_url: Optional[str], arq_redis
    ) -> None:
        """Enqueue the send_contract_email ARQ job (fire-and-forget).

        Extracted from ``issue()`` so both ``issue()`` and ``resend()`` trigger
        the identical email job. Takes explicit values rather than re-deriving
        them from the ORM object, so both call sites are provably passing the
        same arguments the original inlined ``issue()`` block used.
        """
        try:
            await arq_redis.enqueue_job(
                "send_contract_email",
                str(tenant_id),
                str(contract_id),
                payment_link_url,
            )
        except Exception as exc:  # noqa: BLE001
            logger.warning(
                "[_enqueue_contract_email] Failed to enqueue send_contract_email for contract %s: %s",
                contract_id,
                exc,
            )

    @staticmethod
    async def _build_payment_url(
        db: AsyncSession,
        tenant_id: str,
        token: str,
        app_domain: str,
    ) -> str:
        """Resolve tenant subdomain and build the payment link URL.

        Uses PORTAL_BASE_DOMAIN when set (e.g. 'localhost:3001' for local dev
        or 'uat.indelis.com' for UAT). Falls back to APP_DOMAIN.
        Protocol is http for development environment, https everywhere else.
        """
        from src.apps.tenants.models.account import Account
        acct_result = await db.execute(
            select(Account).where(Account.id == tenant_id)
        )
        account = acct_result.scalar_one_or_none()
        subdomain = account.subdomain if account else str(tenant_id)

        portal_domain = getattr(settings, "PORTAL_BASE_DOMAIN", "") or app_domain
        is_dev = getattr(settings, "ENVIRONMENT", "production") == "development"
        protocol = "http" if is_dev else "https"
        return f"{protocol}://{subdomain}.{portal_domain}/pay/{token}"

    @staticmethod
    async def _advance_opportunity_stage(
        db: AsyncSession,
        tenant_id: str,
        opportunity_id: Optional[UUID],
    ) -> bool:
        """Force-set linked opportunity to contract_sent. Returns True if updated."""
        if not opportunity_id:
            return False
        result = await db.execute(
            select(Opportunity).where(
                Opportunity.id == opportunity_id,
                Opportunity.tenant_id == tenant_id,
                Opportunity.deleted_at.is_(None),
            )
        )
        opp = result.scalar_one_or_none()
        if opp and opp.stage not in ("lost", "fully_paid"):
            opp.stage = "contract_sent"
            opp.stage_entered_at = datetime.now(timezone.utc)
            await db.flush()
            return True
        return False
