import logging
from datetime import datetime
from typing import Dict, List, Optional, Union

from fastapi import HTTPException, status
from sqlalchemy import and_, func, or_
from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import Session, joinedload
from sqlalchemy.orm.query import Query

from src.apps.role_permission.models.permission_model import Permission
from src.apps.role_permission.models.role_model import Role, roles_permissions_map
from src.apps.role_permission.models.user_permission_model import UserPermissions
from src.apps.role_permission.schemas.common import PermissionSchema, RoleSchema
from src.apps.role_permission.schemas.requests import (
    RoleCreateRequestSchema,
    RoleListFilterSchema,
    RoleUpdateRequestSchema,
)
from src.apps.user.models.user_model import Users
from src.core.config import settings
from src.utils.constants import API_PREFIXES
from src.utils.helpers.functions import generate_unique_slug
from src.utils.helpers.pagination import QueryPaginator
from src.utils.template_seeder import RoleSeeder

logger = logging.getLogger(__name__)


async def list_roles(
    db: Session,
    filters: RoleListFilterSchema,
    fields: str,
    sort_by: List[str],
    page: int = 1,
    per_page: int = 10,
    current_user: Users = None,
) -> Dict:
    """
    List roles with pagination, filtering, and sorting.

    Args:
    db: Database session
    filters: Filter parameters
    fields: Additional related fields to include
    sort_by: List of fields to sort by (prefix with - for descending order)
    page: Page number for pagination
    per_page: Items per page
    current_user: Current authenticated user information
    """

    allowed_joins = ["permissions", "created_by"]

    db_query = db.query(Role)

    if current_user:
        self_role_ids: List[int] = await get_user_role_ids(db=db, user_id=current_user.user_id)
        if filters.show_all_roles == False:
            """Show all roles including those assigned to the current user"""
            db_query = db_query.filter(Role.id.notin_(self_role_ids))

    if fields:
        sql_joins = fields.split(",")
        for j in sql_joins:
            if j not in allowed_joins:
                raise HTTPException(
                    status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
                    detail=f"Cannot fetch unidentified field '{j}'. Allowed fields are {', '.join(allowed_joins)}",
                )
            db_query = db_query.options(joinedload(getattr(Role, j)))

    if filters.search:
        """Search by label or slug"""
        db_query = db_query.filter(
            or_(
                Role.label.ilike(f"%{filters.search}%"),
                Role.slug.ilike(f"%{filters.search}%"),
            )
        )

    sort_query = []
    if sort_by:
        for s in sort_by:
            try:
                col = s.replace("-", "")
                sort_order = getattr(Role, col)

                if s.startswith("-"):
                    sort_order = sort_order.desc()

                sort_query.append(sort_order)
            except:
                raise HTTPException(
                    status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
                    detail=f"Cannot sort with unidentified column '{s}'",
                )

    if sort_query:
        db_query = db_query.order_by(*sort_query)

    offset = (page - 1) * per_page
    paginator = QueryPaginator(
        query=db_query,
        schema=RoleSchema,
        url="".join([str(settings.api_base_url()), API_PREFIXES.ROLE_PERMISSION]),
        offset=offset,
        limit=per_page,
    )

    result = paginator.paginate()
    result["allowed_fields"] = allowed_joins

    return result


async def list_permissions(
    db: Session, filters: RoleListFilterSchema, sort_by: str, current_user: Users
) -> List[PermissionSchema]:
    """
    Permissions Master List
    """
    db_query = db.query(Permission).filter(Permission.is_active == True)

    user_role = await get_user_role_slugs(db=db, user_id=current_user.user_id)
    if "super_admin" not in user_role:
        user_permissions = await get_user_permission_ids(db=db, user_id=current_user.user_id)
        db_query = db_query.filter(Permission.id.in_(user_permissions))

    if filters.search:
        """Search by label or slug"""
        db_query = db_query.filter(
            or_(
                Permission.module.ilike(f"%{filters.search}%"),
                Permission.operation.ilike(f"%{filters.search}%"),
                Permission.operation_label.ilike(f"%{filters.search}%"),
            )
        )

    sort_query = []
    if len(sort_by) > 0:
        sort_by_list = sort_by.split(",")

        for s in sort_by_list:
            try:
                col = s.replace("-", "")
                sort_order = getattr(Permission, col)

                if s.startswith("-"):
                    sort_order = sort_order.desc()

                sort_query.append(sort_order)
            except Exception:
                raise HTTPException(
                    status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
                    detail=f"Cannot sort with unidentified column '{s}'",
                )

    if sort_query:
        db_query = db_query.order_by(*sort_query)

    results = [PermissionSchema.model_validate(perm) for perm in db_query.all()]
    return results


async def fetch_role_by_slug(db: Session, slug: str, return_raw: bool = False) -> Union[RoleSchema, Role]:
    """
    Fetch a role by its slug
    """
    role = db.query(Role).filter(func.lower(Role.slug) == slug.lower()).first()
    if role:
        if return_raw:
            return role
        return RoleSchema.model_validate(role)
    raise HTTPException(
        status_code=status.HTTP_404_NOT_FOUND,
        detail="No such role found",
    )


async def create_role(
    db: Session,
    payload: RoleCreateRequestSchema,
    current_user: Users,
    return_raw: bool = False,
    is_seeding: bool = False,
    use_provided_slug: bool = False,
) -> Union[RoleSchema, Role]:
    """
    Create a new role (synchronous version)

    This function is used for seeding data during app startup.

    Args:
        db: Database session
        payload: Role creation data
        current_user: User performing the action
        return_raw: Whether to return the raw DB model or schema
        is_seeding: Whether this is being called during initial seeding
        use_provided_slug: Whether to use the slug provided in payload instead of generating
    """
    try:
        if hasattr(payload, "slug") and payload.slug and use_provided_slug:
            existing = db.query(Role).filter(Role.slug == payload.slug).first()
            if existing:
                if is_seeding:
                    logger.info(f"Role with slug {payload.slug} already exists, skipping creation")
                    return existing if return_raw else RoleSchema.model_validate(existing)
                else:
                    raise HTTPException(
                        status_code=status.HTTP_400_BAD_REQUEST, detail=f"Role with slug {payload.slug} already exists"
                    )
            role_slug = payload.slug
        else:
            role_slug = generate_unique_slug(db=db, instance=Role, slug_label=payload.label)

        new_role = Role(
            label=payload.label,
            slug=role_slug,
            is_default=payload.is_default or False,
            created_at=datetime.now(),
            created_by_id=None if is_seeding else current_user.id,
        )

        db.add(new_role)
        db.commit()
        db.refresh(new_role)

        permission_map_statement = roles_permissions_map.insert().values(
            [{"role_id": new_role.id, "permission_id": pid} for pid in payload.permissions]
        )
        db.execute(permission_map_statement)
        db.commit()
        db.refresh(new_role)

        if payload.users and len(payload.users) > 0:
            await assign_role_to_users(db=db, role_id=new_role.id, user_ids=payload.users)

        if return_raw:
            return new_role
        return RoleSchema.model_validate(new_role)

    except IntegrityError:
        raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Attempting to create duplicate role")
    except Exception as e:
        raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=f"Cannot create role: {str(e)}")


async def update_role(
    db: Session,
    slug: str,
    payload: RoleUpdateRequestSchema,
    current_user: Users,
) -> RoleSchema:
    """
    Update a role
    """
    role = await fetch_role_by_slug(db=db, slug=slug, return_raw=True)

    if role.is_default:
        raise HTTPException(
            status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
            detail="Default role cannot be modified or deleted",
        )

    role.permissions = []
    db.commit()

    permission_map_statement = roles_permissions_map.insert().values(
        [{"role_id": role.id, "permission_id": pid} for pid in payload.permissions]
    )
    db.execute(permission_map_statement)
    db.commit()
    db.refresh(role)

    if payload.users and len(payload.users) > 0:
        await assign_role_to_users(db=db, role_id=role.id, user_ids=payload.users)

    try:
        if payload.label is not None:
            role.label = payload.label
        if payload.is_default is not None:
            role.is_default = payload.is_default

        db.commit()
        db.refresh(role)

        return RoleSchema.model_validate(role)
    except Exception as e:
        raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=f"Cannot update role: {str(e)}")


async def remove_role(db: Session, slugs: str) -> Dict:
    """
    Remove a role entry by slug
    """
    try:
        default_role_slugs = [x.slug for x in db.query(Role).filter(Role.is_default == True).all()]

        to_delete = [s.strip() for s in slugs.split(",")]

        for slug in to_delete:
            role = db.query(Role).filter(Role.slug == slug).first()
            if not role:
                raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"Role with slug '{slug}' not found.")

            user_permission = db.query(UserPermissions).filter(UserPermissions.role_id == role.id).first()

            if slug in default_role_slugs:
                raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Cannot delete default role.")
            elif user_permission:
                raise HTTPException(
                    status_code=status.HTTP_412_PRECONDITION_FAILED, detail="The role is assigned to user."
                )

        db.query(Role).filter(Role.slug.in_(to_delete)).delete(synchronize_session=False)
        db.commit()

        return {"deleted": True}
    except HTTPException:
        raise
    except Exception as e:
        raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=f"Cannot delete role: {str(e)}")


async def assign_role_to_users(db: Session, role_id: int, user_ids: List[str] = []) -> None:
    """
    Assign roles to users (synchronous version)

    Args:
        db: Database session
        role_id: ID of the role to assign
        user_ids: List of user IDs to assign the role to
    """
    new_role = db.query(Role).filter(Role.id == role_id).first()

    if not new_role:
        if not user_ids:
            print(f"Warning: Role with ID {role_id} not found, but no users to assign.")
            return
        else:
            raise HTTPException(
                status_code=status.HTTP_400_BAD_REQUEST, detail=f"No such role found with id: {role_id}"
            )

    user_permission_records = []
    for user_id in user_ids:
        user_record = db.query(Users).filter(Users.user_id == user_id).first()

        if not user_record:
            continue

        role_already_assigned = (
            db.query(UserPermissions)
            .filter(
                UserPermissions.user_id == user_record.id,
                UserPermissions.role_id == new_role.id,
            )
            .first()
        )

        if not role_already_assigned:
            user_permission_records.append(UserPermissions(user_id=user_record.id, role_id=new_role.id))

    try:
        if user_permission_records:
            db.bulk_save_objects(user_permission_records)
            db.commit()
    except Exception as e:
        raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=f"Cannot assign role: {str(e)}")


async def run_map_role_permissions(db: Session, keys: str = None) -> Dict:
    """
    Reset role permissions
    """
    permission = (
        db.query(Permission).filter(Permission.is_active == True, Permission.operation == "invoice_view_cost").first()
    )

    if not permission:
        return {"reset": False, "error": "Permission not found"}

    default_role_ids = [x.id for x in db.query(Role).filter(Role.slug == "owner").all()]

    if not default_role_ids:
        return {"reset": False, "error": "No default roles found"}

    try:
        permission_map_statement = roles_permissions_map.insert(
            [{"role_id": role_id, "permission_id": permission.id} for role_id in default_role_ids]
        )
        db.execute(permission_map_statement)
        db.commit()

        return {"reset": True}
    except Exception as e:
        return {"reset": False, "error": str(e)}


async def get_user_role_ids(db: Session, user_id: str) -> List[int]:
    """Get user's role IDs"""
    user = db.query(Users).filter(Users.user_id == user_id).first()
    if not user:
        return []

    user_permissions = db.query(UserPermissions).filter(UserPermissions.user_id == user.id).all()

    return [up.role_id for up in user_permissions]


async def get_user_role_slugs(db: Session, user_id: str) -> List[str]:
    """Get user's role slugs"""
    role_ids = await get_user_role_ids(db=db, user_id=user_id)
    if not role_ids:
        return []

    roles = db.query(Role).filter(Role.id.in_(role_ids)).all()
    return [role.slug for role in roles if hasattr(role, "slug")]


async def get_user_permission_ids(db: Session, user_id: str) -> List[int]:
    """Get user's permission IDs"""
    user = db.query(Users).filter(Users.user_id == user_id).first()
    if not user:
        return []

    direct_permissions = (
        db.query(UserPermissions)
        .filter(UserPermissions.user_id == user.id, UserPermissions.permission_id != None)
        .all()
    )

    direct_permission_ids = [up.permission_id for up in direct_permissions]

    role_ids = await get_user_role_ids(db=db, user_id=user_id)

    role_permissions = db.query(roles_permissions_map).filter(roles_permissions_map.c.role_id.in_(role_ids)).all()

    role_permission_ids = [rp.permission_id for rp in role_permissions]

    all_permission_ids = list(set(direct_permission_ids + role_permission_ids))

    return all_permission_ids


def seed_default_roles(db: Session):
    """
    Seed default templates: welcome user, verify user, and forgot password.
    """
    roles = RoleSeeder.roles

    for role in roles:
        exists = (
            db.query(Role).filter(Role.slug == role["key"]).first()
        )
        if not exists:
            new_role = Role(
                slug=role["key"],
                label=role["name"],
                is_default=role["is_default"]
            )
            db.add(new_role)
    db.commit()

    return {"message": "Default data seeded successfully."}
