"""Bank details service — tenant EFT/wire payment instructions.

One row per tenant (upsert-in-place, like Account/BrandingConfig — see
_load_account_with_branding/update_profile in settings/router.py).
`account_number` is encrypted at rest (Fernet, src/core/encryption.py);
`account_number_last4` is kept in plaintext alongside it purely so a last-4
hint can be shown without a decrypt round-trip (e.g. in logs/audit entries).

Both `account_number_encrypted` and `account_number_last4` are written in
exactly one place — `BankDetailsService.upsert` — and must stay that way; the
DB has no constraint tying the two together (see DBA doc §5), so a second
write path would silently desync them.

`get_decrypted_for_tenant` is the read path for the Settings page, the
authenticated contract PDF/download, and every transactional email — the
purchaser needs the real account number to actually complete an EFT/wire
transfer. `get_masked_for_tenant`/`to_masked` remain for exactly one
caller: the PUBLIC, unauthenticated `GET /public/contracts/{subdomain}/
{contract_number}/pdf` QR-code endpoint, whose contract_number is
sequential/guessable and has no auth or rate limit — showing the real
account number there would let anyone who can guess a contract number read
the cemetery's live bank details.
"""
from __future__ import annotations

import logging
from typing import Optional

from cryptography.fernet import InvalidToken
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession

from src.apps.settings.models.bank_details import BankDetails
from src.apps.settings.schemas.requests import BankDetailsUpsertRequest
from src.core.encryption import decrypt_value, encrypt_value

logger = logging.getLogger(__name__)


class BankDetailsService:

    @staticmethod
    async def upsert(
        db: AsyncSession, tenant_id, payload: BankDetailsUpsertRequest
    ) -> BankDetails:
        existing = (
            await db.execute(
                select(BankDetails).where(BankDetails.tenant_id == tenant_id)
            )
        ).scalar_one_or_none()

        encrypted = encrypt_value(payload.account_number)
        last4 = payload.account_number[-4:]

        if existing is None:
            record = BankDetails(
                tenant_id=tenant_id,
                account_holder_name=payload.account_holder_name,
                bank_name=payload.bank_name,
                institution_number=payload.institution_number,
                transit_number=payload.transit_number,
                account_number_encrypted=encrypted,
                account_number_last4=last4,
                account_type=payload.account_type,
                currency=payload.currency,
                branch_address=payload.branch_address,
            )
            db.add(record)
        else:
            existing.account_holder_name = payload.account_holder_name
            existing.bank_name = payload.bank_name
            existing.institution_number = payload.institution_number
            existing.transit_number = payload.transit_number
            existing.account_number_encrypted = encrypted
            existing.account_number_last4 = last4
            existing.account_type = payload.account_type
            existing.currency = payload.currency
            existing.branch_address = payload.branch_address
            record = existing

        await db.flush()
        return record

    @staticmethod
    def to_masked(last4: str) -> str:
        """Fixed-width mask (does not reveal whether the real number was
        7 or 12 digits). Plain ASCII asterisks — must render correctly in
        both ReportLab PDF text and plain-text email. Used only by the
        public, unauthenticated contract-PDF endpoint (see module
        docstring) — every other caller must use get_decrypted_for_tenant."""
        return f"****{last4}"

    @staticmethod
    async def get_masked_for_tenant(db: AsyncSession, tenant_id) -> Optional[dict]:
        """Same dict shape as get_decrypted_for_tenant(), but account_number
        is masked rather than decrypted — no Fernet decrypt call. Used only
        by the public, unauthenticated contract-PDF endpoint (see module
        docstring)."""
        bank = (
            await db.execute(
                select(BankDetails).where(BankDetails.tenant_id == tenant_id)
            )
        ).scalar_one_or_none()
        if bank is None:
            return None
        return {
            "id": bank.id,
            "tenant_id": bank.tenant_id,
            "account_holder_name": bank.account_holder_name,
            "bank_name": bank.bank_name,
            "institution_number": bank.institution_number,
            "transit_number": bank.transit_number,
            "account_number": BankDetailsService.to_masked(bank.account_number_last4),
            "account_type": bank.account_type,
            "currency": bank.currency,
            "branch_address": bank.branch_address,
            "created_at": bank.created_at,
            "updated_at": bank.updated_at,
        }

    @staticmethod
    async def get_decrypted_for_tenant(db: AsyncSession, tenant_id) -> Optional[dict]:
        """Fetch a tenant's bank details with the account number decrypted
        to plaintext. Returns a plain dict (never the ORM row) — this is the
        single read path used by GET /settings/bank-details, the contract
        PDF, and send_contract_email.

        Swallows decrypt failures (bad/rotated key, missing/misconfigured
        BANK_DETAILS_ENCRYPTION_KEY) by logging and returning None, so
        callers degrade to their own "not configured" fallback rather than
        raising.
        """
        bank = (
            await db.execute(
                select(BankDetails).where(BankDetails.tenant_id == tenant_id)
            )
        ).scalar_one_or_none()
        if bank is None:
            return None
        try:
            account_number = decrypt_value(bank.account_number_encrypted)
        except (InvalidToken, RuntimeError):
            logger.warning("bank_details decrypt failed for tenant_id=%s", tenant_id)
            return None
        return {
            "id": bank.id,
            "tenant_id": bank.tenant_id,
            "account_holder_name": bank.account_holder_name,
            "bank_name": bank.bank_name,
            "institution_number": bank.institution_number,
            "transit_number": bank.transit_number,
            "account_number": account_number,
            "account_type": bank.account_type,
            "currency": bank.currency,
            "branch_address": bank.branch_address,
            "created_at": bank.created_at,
            "updated_at": bank.updated_at,
        }
