"""
User management business logic service.
"""

import asyncio
import math
from uuid import UUID

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

from app.core.exceptions import (
    AlreadyExistsException,
    ForbiddenException,
    NotFoundException,
    ValidationException,
)
from app.core.pagination import PaginationParams
from app.core.responses import PaginatedResponse, PaginationMeta
from app.core.security import hash_password, verify_password
from app.models.audit import AuditCategory, AuditSeverity
from app.models.user import Role, User, UserRole
from app.repositories.lab_repository import LabRepository
from app.repositories.user_repository import UserRepository
from app.schemas.user import (
    ChangePasswordRequest,
    UserCreate,
    UserResponse,
    UserUpdate,
)
from app.services.audit_service import AuditService


class UserService:
    """Service handling business logic for User operations."""

    def __init__(
        self,
        repository: UserRepository | None = None,
        audit_service: AuditService | None = None,
    ) -> None:
        self.repo = repository or UserRepository()
        self.audit = audit_service or AuditService()

    async def _record_staff_event(
        self,
        db: AsyncSession,
        *,
        action: str,
        description: str,
        user: User,
        actor: User | None,
        details: dict[str, object] | None = None,
        category: str = AuditCategory.ADMIN.value,
        severity: str = AuditSeverity.INFO.value,
    ) -> None:
        """Write staff account changes into the same transaction as the mutation."""
        actor_role = getattr(actor.role, "value", actor.role) if actor else "system"
        await self.audit.record_event(
            db,
            action=action,
            description=description,
            category=category,
            severity=severity,
            actor_name=actor.name if actor else "System",
            actor_role=str(actor_role),
            actor_id=actor.id if actor else None,
            target_type="staff_user",
            target_id=str(user.id),
            ip_address=None,
            details=details,
            lab_id=user.lab_id,
        )

    async def _ensure_role_exists(self, db: AsyncSession, role: str) -> None:
        """Role must be a built-in UserRole value or a superadmin-created Role row."""
        if role in UserRole.codes():
            return
        res = await db.execute(select(Role.id).where(Role.code == role))
        if res.scalar_one_or_none() is None:
            raise NotFoundException(
                message=f"Role '{role}' does not exist. Create it first via role management."
            )

    # Roles that must carry a professional council registration number.
    LICENSE_REQUIRED_ROLES = frozenset(
        {UserRole.DOCTOR.value, UserRole.PATHOLOGIST.value, UserRole.RADIOLOGIST.value}
    )

    def _require_license_no(self, role: str, license_no: str | None) -> None:
        if role in self.LICENSE_REQUIRED_ROLES and not (license_no or "").strip():
            raise ValidationException(
                message=(
                    "Council registration (NMC) number is mandatory"
                    " for doctor / pathologist / radiologist accounts."
                )
            )

    async def get_user(self, db: AsyncSession, user_id: UUID) -> UserResponse:
        """Fetch user by ID or raise NotFoundException."""
        user = await self.repo.get_by_id(db, user_id)
        if not user:
            raise NotFoundException(message=f"User with id '{user_id}' was not found.")
        return UserResponse.model_validate(user)

    async def list_users(
        self,
        db: AsyncSession,
        params: PaginationParams,
        lab_id: UUID | None = None,
        role: UserRole | None = None,
        is_active: bool | None = None,
    ) -> PaginatedResponse[UserResponse]:
        """Fetch paginated users matching criteria."""
        items, total = await self.repo.list_users(
            db,
            limit=params.limit,
            offset=params.offset,
            lab_id=lab_id,
            role=role,
            is_active=is_active,
        )
        pages = math.ceil(total / params.limit) if total > 0 else 1
        return PaginatedResponse(
            data=[UserResponse.model_validate(user) for user in items],
            meta=PaginationMeta(
                total=total,
                page=params.page,
                limit=params.limit,
                pages=pages,
            ),
        )

    async def create_user(
        self, db: AsyncSession, data: UserCreate, creator: User | None = None
    ) -> UserResponse:
        """Register a new staff user with uniqueness checks and password hashing."""
        # Only existing superadmins can create additional superadmins
        if data.role == UserRole.SUPERADMIN and (creator is None or not creator.is_superadmin):
            raise ForbiddenException(
                message="Only existing superadmins have permission to create another superadmin."
            )

        if data.lab_id is None:
            data.lab_id = await LabRepository().get_default_lab_id(db)
        else:
            lab = await LabRepository().get_by_id(db, data.lab_id)
            if not lab:
                raise NotFoundException(message=f"Lab with id '{data.lab_id}' does not exist.")

        await self._ensure_role_exists(db, data.role)
        self._require_license_no(data.role, data.license_no)
        if data.email == "":
            data.email = None

        # Uniqueness check: phone per lab
        existing_phone = await self.repo.get_by_phone(db, phone=data.phone, lab_id=data.lab_id)
        if existing_phone:
            raise AlreadyExistsException(
                message=f"A user with phone '{data.phone}' already exists in this lab."
            )

        # Uniqueness check: email per lab (if provided)
        if data.email:
            existing_email = await self.repo.get_by_email(db, email=data.email, lab_id=data.lab_id)
            if existing_email:
                raise AlreadyExistsException(
                    message=f"A user with email '{data.email}' already exists in this lab."
                )

        # Re-hire: a soft-deleted row still holds the unique (lab_id, phone).
        # Restore it with the new-hire payload instead of 500ing on the
        # unique constraint — history (audit trails) stays intact.
        archived = await self.repo.get_by_phone_including_deleted(db, phone=data.phone, lab_id=data.lab_id)
        if archived is not None and archived.deleted_at is not None:
            user = await self.repo.restore_from_create(db, archived, data)
            await self._record_staff_event(
                db,
                action="STAFF_CREATED",
                description=f"Reactivated staff account for {user.name}.",
                user=user,
                actor=creator,
                details={"role": str(getattr(user.role, "value", user.role)), "rehire": True},
            )
            return UserResponse.model_validate(user)

        hashed_pwd = await asyncio.to_thread(hash_password, data.password)
        user = await self.repo.create(db, data, hashed_password=hashed_pwd)
        await self._record_staff_event(
            db,
            action="STAFF_CREATED",
            description=f"Created staff account for {user.name}.",
            user=user,
            actor=creator,
            details={"role": str(getattr(user.role, "value", user.role)), "rehire": False},
        )
        return UserResponse.model_validate(user)

    async def update_user(
        self,
        db: AsyncSession,
        user_id: UUID,
        data: UserUpdate,
        actor: User | None = None,
    ) -> UserResponse:
        """Update an existing user's details."""
        user = await self.repo.get_by_id(db, user_id)
        if not user:
            raise NotFoundException(message=f"User with id '{user_id}' was not found.")

        # Only superadmins can assign or promote to the superadmin role
        if data.role == UserRole.SUPERADMIN and (actor is None or not actor.is_superadmin):
            raise ForbiddenException(
                message="Only existing superadmins have permission to grant the superadmin role."
            )

        if data.role is not None:
            await self._ensure_role_exists(db, data.role)
        effective_role = data.role if data.role is not None else user.role
        effective_license = data.license_no if "license_no" in data.model_fields_set else user.license_no
        if data.role is not None or "license_no" in data.model_fields_set:
            self._require_license_no(effective_role, effective_license)
        if data.email == "":
            data.email = None

        if data.phone and data.phone != user.phone:
            existing_phone = await self.repo.get_by_phone(db, phone=data.phone, lab_id=user.lab_id)
            if existing_phone and existing_phone.id != user.id:
                raise AlreadyExistsException(
                    message=f"A user with phone '{data.phone}' already exists in this lab."
                )

        if data.email and data.email != user.email:
            existing_email = await self.repo.get_by_email(db, email=data.email, lab_id=user.lab_id)
            if existing_email and existing_email.id != user.id:
                raise AlreadyExistsException(
                    message=f"A user with email '{data.email}' already exists in this lab."
                )

        updated = await self.repo.update(db, user, data)
        changed_fields = sorted(data.model_fields_set)
        if changed_fields:
            await self._record_staff_event(
                db,
                action="STAFF_UPDATED",
                description=f"Updated staff account for {updated.name}.",
                user=updated,
                actor=actor,
                details={"changed_fields": changed_fields},
            )
        return UserResponse.model_validate(updated)

    async def activate_user(self, db: AsyncSession, user_id: UUID, actor: User | None = None) -> UserResponse:
        """Activate a staff member account."""
        user = await self.repo.get_by_id(db, user_id)
        if not user:
            raise NotFoundException(message=f"User with id '{user_id}' was not found.")

        was_active = user.is_active
        updated = await self.repo.set_active_status(db, user, is_active=True)
        if not was_active:
            await self._record_staff_event(
                db,
                action="STAFF_ACTIVATED",
                description=f"Activated staff account for {updated.name}.",
                user=updated,
                actor=actor,
            )
        return UserResponse.model_validate(updated)

    async def deactivate_user(
        self, db: AsyncSession, user_id: UUID, actor: User | None = None
    ) -> UserResponse:
        """Deactivate a staff member account."""
        user = await self.repo.get_by_id(db, user_id)
        if not user:
            raise NotFoundException(message=f"User with id '{user_id}' was not found.")
        if actor and actor.id == user.id:
            raise ValidationException(message="You cannot deactivate your own account.")
        if user.is_superadmin and (actor is None or not actor.is_superadmin):
            raise ForbiddenException(message="Only superadmins can deactivate a superadmin account.")

        was_active = user.is_active
        updated = await self.repo.set_active_status(db, user, is_active=False)
        if was_active:
            await self._record_staff_event(
                db,
                action="STAFF_DEACTIVATED",
                description=f"Deactivated staff account for {updated.name}.",
                user=updated,
                actor=actor,
            )
        return UserResponse.model_validate(updated)

    async def delete_user(self, db: AsyncSession, user_id: UUID, actor: User | None = None) -> None:
        """Soft-delete a user account."""
        user = await self.repo.get_by_id(db, user_id)
        if not user:
            raise NotFoundException(message=f"User with id '{user_id}' was not found.")
        if actor and actor.id == user.id:
            raise ValidationException(message="You cannot delete your own account.")
        if user.is_superadmin and (actor is None or not actor.is_superadmin):
            raise ForbiddenException(message="Only superadmins can delete a superadmin account.")
        await self.repo.delete(db, user)
        await self._record_staff_event(
            db,
            action="STAFF_DELETED",
            description=f"Deleted staff account for {user.name}.",
            user=user,
            actor=actor,
        )

    async def change_password(self, db: AsyncSession, user: User, data: ChangePasswordRequest) -> None:
        """Change current user's password verifying old password first."""
        if not await asyncio.to_thread(verify_password, data.old_password, user.hashed_password):
            raise ValidationException(message="Current password does not match.")

        new_hashed = await asyncio.to_thread(hash_password, data.new_password)
        await self.repo.update_password(db, user, new_hashed)
        await self._record_staff_event(
            db,
            action="STAFF_PASSWORD_CHANGED",
            description=f"Staff member {user.name} changed their password.",
            user=user,
            actor=user,
            category=AuditCategory.SECURITY.value,
        )

    async def reset_password(
        self, db: AsyncSession, user_id: UUID, new_password: str, actor: User | None = None
    ) -> UserResponse:
        """Superadmin-only: set any staff member's password without the old one."""
        if actor is None or not actor.is_superadmin:
            raise ForbiddenException(message="Only superadmins can reset staff passwords.")
        user = await self.repo.get_by_id(db, user_id)
        if not user:
            raise NotFoundException(message=f"User with id '{user_id}' was not found.")
        await self.repo.update_password(db, user, await asyncio.to_thread(hash_password, new_password))
        await self._record_staff_event(
            db,
            action="STAFF_PASSWORD_RESET",
            description=f"Reset password for staff account {user.name}.",
            user=user,
            actor=actor,
            details={"initiated_by_admin": True},
            category=AuditCategory.SECURITY.value,
            severity=AuditSeverity.WARNING.value,
        )
        return UserResponse.model_validate(user)
