"""Resolve the Addons provider for a specific organization.

WHY THIS EXISTS
---------------
``ProviderAddons()`` used to fall back to ``config.ADDONS_API_KEY`` — the platform
owner's own seller account — whenever no key was passed. The Celery tasks decrypted
the org's key and passed it correctly, and ``sync_tasks.py`` even carries the
warning *"NEVER fall back to the global key for other tenants: that key is the
owner's account and would cross-contaminate their data."* But the whole request
path (``/sync``, ticket GET/POST, historical sync, order sync, autoresponses) built
the provider with no arguments, so every tenant operated against the owner's
account: inbound, their threads were imported into the tenant's schema; outbound,
a tenant's reply could be posted onto the owner's Addons thread.

The rule now has exactly one implementation:

* every org — **including the internal/owner org** — stores its own encrypted key
* a request resolves the provider through :func:`provider_for_org`
* an org with no key raises :class:`MissingAddonsKey` instead of silently
  borrowing someone else's account

Failing loudly is the point. A tenant seeing "connect your Addons account" is a
support ticket; a tenant silently reading and replying as us is a data breach.
"""
from __future__ import annotations

import logging
from typing import Optional

from ..config import config
from ..providers.addons import ProviderAddons

logger = logging.getLogger(__name__)


class MissingAddonsKey(RuntimeError):
    """Raised when an org has no usable Addons API key.

    Deliberately NOT caught-and-defaulted anywhere: the whole point is that the
    absence of a key stops the operation rather than redirecting it at the owner's
    account.
    """

    def __init__(self, org_id=None):
        self.org_id = org_id
        super().__init__(
            f"Organization {org_id!r} has no Addons API key configured. "
            "Connect an Addons account in Settings before syncing."
        )


def decrypt_addons_key(org) -> Optional[str]:
    """Return the org's decrypted Addons API key, or None.

    Tolerates both str and bytes ciphertext: the column is Text and older rows may
    hold either (see _encrypt in onboarding/routes.py).
    """
    raw = getattr(org, "addons_api_key_encrypted", None)
    if not raw:
        return None
    fernet_key = config.CREDENTIAL_ENCRYPTION_KEY
    if not fernet_key:
        # No platform key configured: _encrypt stored the plaintext as-is.
        return raw.decode() if isinstance(raw, bytes) else raw
    try:
        from cryptography.fernet import Fernet
        f = Fernet(fernet_key.encode() if isinstance(fernet_key, str) else fernet_key)
        if isinstance(raw, str):
            raw = raw.encode()
        return f.decrypt(raw).decode()
    except Exception as exc:
        logger.error("Could not decrypt Addons key for org %s: %s",
                     getattr(org, "id", "?"), exc)
        return None


def provider_for_org(org) -> ProviderAddons:
    """Build a provider bound to this org's own Addons account.

    Raises MissingAddonsKey if the org has none. There is no global fallback —
    that fallback WAS the bug.
    """
    if org is None:
        raise MissingAddonsKey(None)
    key = decrypt_addons_key(org)
    if not key:
        raise MissingAddonsKey(getattr(org, "id", None))
    return ProviderAddons(api_key=key, base_url=config.ADDONS_API_BASE_URL)


def provider_for_current_user() -> ProviderAddons:
    """Resolve the provider for the logged-in user's organization."""
    from flask_login import current_user
    from ..db import session_scope
    from ..public_models import Organization

    org_id = getattr(current_user, "org_id", None)
    with session_scope(schema="public") as pub:
        org = pub.query(Organization).filter_by(id=org_id).one_or_none()
        if org is None:
            raise MissingAddonsKey(org_id)
        # Decrypt inside the session, return a detached-safe provider.
        return provider_for_org(org)
