"""
Just-In-Time user provisioning.

When an external strategy authenticates successfully but the user does
not exist locally, this module creates a stub. If a local user with the
same email exists, it auto-links (per user preference in the conversation
that spawned this work: auto-link on first SSO).

NEVER stores remote credentials. `password_hash` stays NULL for external
users — local password login is impossible for those accounts.
"""
from __future__ import annotations

import json
import logging
from datetime import datetime
from typing import Optional

from sqlalchemy.orm import Session

from app.auth.role_mapper import map_groups_to_role
from app.auth.strategies.base import ExternalIdentity
from app.models.audit_log import AuditEventType, AuditLog
from app.models.user import AuthProvider, User, UserRole

logger = logging.getLogger(__name__)


class JITProvisioner:
    """
    Provision or update a local User row from an `ExternalIdentity`.

    Lookup order on each authenticate:
    1. By (auth_provider, external_id) — stable IdP subject
    2. By email — auto-link existing local user (per user preference)
    3. JIT create new stub
    """

    def __init__(self, db: Session, *, default_role: UserRole = UserRole.READONLY):
        self.db = db
        self.default_role = default_role

    def provision(
        self,
        identity: ExternalIdentity,
        *,
        actor_ip: Optional[str] = None,
    ) -> User:
        # 1) Stable provider+subject match
        user = (
            self.db.query(User)
            .filter(
                User.auth_provider == identity.provider,
                User.external_id == identity.external_id,
            )
            .first()
        )

        # 2) Auto-link by email (only first time — afterwards step 1 succeeds)
        if user is None and identity.email:
            existing_local = (
                self.db.query(User).filter(User.email == identity.email).first()
            )
            if existing_local is not None:
                user = self._auto_link(existing_local, identity, actor_ip)

        # 3) JIT create
        if user is None:
            user = self._jit_create(identity, actor_ip)

        # Refresh role from current groups every login
        self._refresh_role(user, identity, actor_ip)

        # Touch sync timestamp + groups
        user.external_groups = json.dumps(identity.groups) if identity.groups else None
        user.last_provider_sync = datetime.now()

        self.db.commit()
        self.db.refresh(user)
        return user

    # ---------- internals ----------

    def _jit_create(self, identity: ExternalIdentity, ip: Optional[str]) -> User:
        # Resolve role from groups before insert
        role, matched_pattern = map_groups_to_role(
            self.db, identity.provider, identity.groups, default=self.default_role
        )

        # Defensive: avoid username collision by appending provider suffix if needed.
        base_username = identity.username
        username = base_username
        suffix = 1
        while self.db.query(User).filter(User.username == username).first() is not None:
            username = f"{base_username}.{identity.provider.value}{suffix if suffix > 1 else ''}"
            suffix += 1

        user = User(
            username=username,
            email=identity.email,
            password_hash=None,
            role=role,
            auth_provider=identity.provider,
            external_id=identity.external_id,
            is_active=True,
            is_verified=True,
            failed_login_attempts=0,
        )
        self.db.add(user)
        self.db.flush()  # get user.id

        self.db.add(
            AuditLog(
                user_id=user.id,
                event_type=AuditEventType.JIT_USER_CREATED,
                event_description=(
                    f"JIT-provisioned user '{username}' via {identity.provider.value} "
                    f"(role={role.value}, matched_rule={matched_pattern or 'default'})"
                ),
                ip_address=ip,
                resource_type="user",
                resource_id=str(user.id),
                timestamp=datetime.now(),
            )
        )
        logger.info(
            "JIT user created: %s via %s (role=%s)",
            username,
            identity.provider.value,
            role.value,
        )
        return user

    def _auto_link(
        self, local_user: User, identity: ExternalIdentity, ip: Optional[str]
    ) -> User:
        """Promote an existing local user to be linked with the external IdP."""
        prev_provider = local_user.auth_provider
        local_user.auth_provider = identity.provider
        local_user.external_id = identity.external_id
        # Keep username/email/role — admin already trusted this user.

        self.db.add(
            AuditLog(
                user_id=local_user.id,
                event_type=AuditEventType.USER_AUTO_LINKED,
                event_description=(
                    f"Auto-linked local user '{local_user.username}' to "
                    f"{identity.provider.value} (was {prev_provider.value if prev_provider else 'none'})"
                ),
                ip_address=ip,
                resource_type="user",
                resource_id=str(local_user.id),
                timestamp=datetime.now(),
            )
        )
        logger.info(
            "Auto-linked existing user %s to %s",
            local_user.username,
            identity.provider.value,
        )
        return local_user

    def _refresh_role(
        self, user: User, identity: ExternalIdentity, ip: Optional[str]
    ) -> None:
        """Re-evaluate role mapping on every login (so admin promotion/demotion in IdP is honored)."""
        new_role, matched = map_groups_to_role(
            self.db, identity.provider, identity.groups, default=self.default_role
        )
        if new_role != user.role:
            old = user.role
            user.role = new_role
            self.db.add(
                AuditLog(
                    user_id=user.id,
                    event_type=AuditEventType.EXTERNAL_ROLE_MAPPED,
                    event_description=(
                        f"Role updated from {old.value} to {new_role.value} via "
                        f"{identity.provider.value} (rule={matched or 'default'})"
                    ),
                    ip_address=ip,
                    resource_type="user",
                    resource_id=str(user.id),
                    old_value=old.value,
                    new_value=new_role.value,
                    timestamp=datetime.now(),
                )
            )
