"""Owner-approved billing delegation for agent connections.

Why this exists: a member has exactly one role, none combines developer access (required to
share a workspace with an agent) with billing.manage, and ``admin`` deliberately excludes
billing. So without this, only the workspace owner could ever put billing scopes on an agent
connection. A member may instead ask the owner; the owner approves or declines; an approved
member may delegate ``billing.read`` and ``credits.checkout`` (never ``credits.purchase``) to
agent connections in that workspace. Nothing about the member's role or permissions changes.

Enforcement is not here: AgentAccessService.can_delegate_scope consults the approved row at
consent time and again on every agent call, so a revoke (or the approving owner losing
ownership) takes effect immediately.
"""

from __future__ import annotations

from datetime import UTC, datetime, timedelta
from typing import Literal
from uuid import UUID, uuid4

import structlog

from src.core.exceptions import ConflictError, ForbiddenError, NotFoundError, ValidationError
from src.domain.agent_access.models import WorkspaceBillingDelegation
from src.domain.agent_access.schemas import MIN_DELEGATION_PERMISSIONS

logger = structlog.get_logger()

Decision = Literal["approve", "decline", "revoke"]
# A declined or revoked member may ask again, but not to nag the owner.
REREQUEST_COOLDOWN = timedelta(hours=24)


def _now() -> datetime:
    return datetime.now(UTC)


def _view(
    row: WorkspaceBillingDelegation, email: str | None = None, name: str | None = None
) -> dict:
    view = {
        "id": str(row.id),
        "workspace_id": str(row.workspace_id),
        "member_id": str(row.member_id),
        "status": row.status,
        "requested_at": row.requested_at,
        "resolved_at": row.resolved_at,
    }
    if email is not None:
        view["member_email"] = email
        view["member_name"] = name or email
    return view


class BillingDelegationService:
    def __init__(self, repo, workspaces, users, email_provider=None, app_url: str = ""):
        self.repo = repo
        self.workspaces = workspaces
        self.users = users
        self.email_provider = email_provider
        self.app_url = app_url.rstrip("/")

    async def _owner_id(self, workspace_id: UUID) -> UUID:
        workspace = await self.workspaces.get_workspace_summary(workspace_id)
        if not workspace or not workspace.owner_id:
            raise NotFoundError("Workspace not found")
        return workspace.owner_id

    async def _require_owner(self, workspace_id: UUID, user_id: UUID) -> None:
        if await self._owner_id(workspace_id) != user_id:
            raise ForbiddenError("Only the workspace owner can manage billing delegation")

    async def request(self, workspace_id: UUID, member_id: UUID) -> dict:
        """A member asks the owner to let them put billing scopes on agent connections."""
        for permission in MIN_DELEGATION_PERMISSIONS:
            if not await self.workspaces.check_permission(workspace_id, member_id, permission):
                # Also covers non-members: only someone who could share the workspace at all
                # has any use for billing delegation.
                raise ForbiddenError("Developer access is required in this workspace")
        if await self.workspaces.check_permission(workspace_id, member_id, "billing.manage"):
            raise ValidationError("You already have billing access in this workspace")
        owner_id = await self._owner_id(workspace_id)
        row = await self.repo.billing_delegation(workspace_id, member_id)
        now = _now()
        if row is None:
            row = await self.repo.add(
                WorkspaceBillingDelegation(
                    id=uuid4(),
                    workspace_id=workspace_id,
                    member_id=member_id,
                    status="pending",
                    requested_at=now,
                    resolved_at=None,
                    resolved_by=None,
                )
            )
        elif row.status in {"pending", "approved"} and not (
            row.status == "approved" and row.resolved_by != owner_id
        ):
            return _view(row)  # idempotent: nothing to change, nobody to notify again
        else:
            # declined, revoked, or approved by an owner who no longer owns the workspace
            if row.resolved_at and now - row.resolved_at < REREQUEST_COOLDOWN:
                raise ConflictError("Please wait before asking again")
            row.status = "pending"
            row.requested_at = now
            row.resolved_at = None
            row.resolved_by = None
        logger.info(
            "billing_delegation_requested",
            workspace_id=str(workspace_id),
            member_id=str(member_id),
        )
        await self._notify_owner(workspace_id, owner_id, member_id)
        return _view(row)

    async def list_for_owner(self, workspace_id: UUID, user_id: UUID) -> list[dict]:
        await self._require_owner(workspace_id, user_id)
        rows = await self.repo.billing_delegations_for_workspace(workspace_id)
        return [_view(row, email, name) for row, email, name in rows]

    async def resolve(
        self, workspace_id: UUID, delegation_id: UUID, owner_id: UUID, decision: Decision
    ) -> dict:
        await self._require_owner(workspace_id, owner_id)
        row = await self.repo.billing_delegation_by_id(delegation_id, workspace_id, lock=True)
        if not row:
            raise NotFoundError("Delegation request not found")
        allowed_from = {"approve": "pending", "decline": "pending", "revoke": "approved"}
        if row.status != allowed_from[decision]:
            raise ConflictError(f"A {row.status} request cannot be {decision}d")
        if decision == "approve":
            # Do not approve someone who has since lost the access that makes it useful.
            for permission in MIN_DELEGATION_PERMISSIONS:
                if not await self.workspaces.check_permission(
                    workspace_id, row.member_id, permission
                ):
                    raise ValidationError("This member no longer has developer access")
        row.status = {"approve": "approved", "decline": "declined", "revoke": "revoked"}[decision]
        row.resolved_at = _now()
        row.resolved_by = owner_id
        logger.info(
            f"billing_delegation_{row.status}",
            workspace_id=str(workspace_id),
            member_id=str(row.member_id),
            resolved_by=str(owner_id),
        )
        return _view(row)

    async def _notify_owner(self, workspace_id: UUID, owner_id: UUID, member_id: UUID) -> None:
        """Best-effort email; the request is already recorded and visible in Settings."""
        if not self.email_provider or not self.app_url:
            return
        try:
            from src.domain.auth.email_templates import build_billing_delegation_request_email

            owner = await self.users.get_by_id(owner_id)
            member = await self.users.get_by_id(member_id)
            workspace = await self.workspaces.get_workspace_summary(workspace_id)
            if not owner or not member or not owner.email:
                return
            subject, html_body, text_plain = build_billing_delegation_request_email(
                requester_display=member.display_name or member.email,
                requester_email=member.email,
                workspace_name=(workspace.name if workspace else "") or "your workspace",
                review_url=f"{self.app_url}/settings/agents",
            )
            await self.email_provider.send(owner.email, subject, html_body, text_plain)
        except Exception as exc:  # never let a mail problem fail the request
            logger.warning(
                "billing_delegation_email_failed",
                workspace_id=str(workspace_id),
                error_type=type(exc).__name__,
            )
