from typing import Optional, Union
import uuid
from datetime import datetime
from sqlalchemy.orm import Session
from sqlalchemy.exc import SQLAlchemyError
import hashlib
import os
import secrets

# Assuming the models are in a file named `models.py` in the `app.db` directory
from app.db import models

def hash_password(password: str, salt: str = None) -> str:
    """Hashes a password using PBKDF2 with SHA256."""
    if not salt:
        salt = secrets.token_hex(16)
    encoded_pw = password.encode('utf-8')
    encoded_salt = salt.encode('utf-8')
    iterations = 100000
    hash_bytes = hashlib.pbkdf2_hmac('sha256', encoded_pw, encoded_salt, iterations)
    return f"{salt}${iterations}${hash_bytes.hex()}"

def verify_password(plain_password: str, hashed_password: str) -> bool:
    """Verifies a plain password against a stored hash."""
    if not hashed_password or '$' not in hashed_password:
        return False
    parts = hashed_password.split('$')
    if len(parts) != 3:
        return False
    salt, iterations, stored_hash = parts
    
    encoded_pw = plain_password.encode('utf-8')
    encoded_salt = salt.encode('utf-8')
    hash_bytes = hashlib.pbkdf2_hmac('sha256', encoded_pw, encoded_salt, int(iterations))
    return hash_bytes.hex() == stored_hash

class UserService:
    def __init__(self):
        pass

    def bootstrap_local_admin(self, db: Session):
        """
        [Day 1] Checks environment for CORTEX_ADMIN_PASSWORD and SUPER_ADMINS.
        If present, creates or updates the primary super admin with local login credentials.
        """
        from app.config import settings
        admin_email = settings.SUPER_ADMINS[0] if settings.SUPER_ADMINS else None
        admin_password = os.getenv("CORTEX_ADMIN_PASSWORD")
        
        if not admin_email or not admin_password:
            return
            
        try:
            admin = db.query(models.User).filter(models.User.email == admin_email).first()
            if admin:
                # Always sync the password when CORTEX_ADMIN_PASSWORD is explicitly set
                # This ensures test runs with a fresh DB credential always work correctly
                admin.password_hash = hash_password(admin_password)
                db.commit()
            else:
                default_group = self.get_or_create_default_group(db)
                new_admin = models.User(
                    id=str(uuid.uuid4()),
                    email=admin_email,
                    username=admin_email.split("@")[0],
                    role="admin",
                    group_id=default_group.id,
                    password_hash=hash_password(admin_password),
                    created_at=datetime.utcnow(),
                    last_login_at=datetime.utcnow()
                )
                db.add(new_admin)
                db.commit()
                db.refresh(new_admin)
                admin = new_admin
                print(f"[Day 1] Bootstrapped local admin account for {admin_email}")
                
        except Exception as e:
            db.rollback()
            print(f"Failed to bootstrap local admin: {e}")

    def sync_oidc_user(self, db: Session, email: str, external_id: str, full_name: str = None, avatar_url: str = None) -> str:
        """
        Syncs an OIDC user with the local database.
        Creates the user if they don't exist, or updates their profile if they do.
        """
        try:
            # 1. Check if user already exists by OIDC sub (external_id)
            user = db.query(models.User).filter(models.User.oidc_id == external_id).first()
            
            # 2. If not found by OIDC ID, check by email (to support linking)
            if not user:
                user = db.query(models.User).filter(models.User.email == email).first()
                if user:
                    # Link existing local user to this OIDC identity
                    user.oidc_id = external_id
            
            if user:
                # Update existing user profile
                user.email = email
                if full_name and not user.full_name:
                    user.full_name = full_name
                if avatar_url:
                    user.avatar_url = avatar_url
                user.last_login_at = datetime.utcnow()
            else:
                # 3. Create new user
                default_group = self.get_or_create_default_group(db)
                from app.config import settings
                role = "admin" if email in settings.SUPER_ADMINS else "user"
                
                user = models.User(
                    id=str(uuid.uuid4()),
                    email=email,
                    username=email.split("@")[0],
                    full_name=full_name,
                    avatar_url=avatar_url,
                    oidc_id=external_id,
                    role=role,
                    group_id=default_group.id,
                    created_at=datetime.utcnow(),
                    last_login_at=datetime.utcnow()
                )
                db.add(user)
            
            db.commit()
            db.refresh(user)
            return user.id
            
        except Exception as e:
            db.rollback()
            print(f"Failed to sync OIDC user {email}: {e}")
            raise e

    def save_user(self, db: Session, oidc_id: str, email: str, username: str, full_name: str = None) -> tuple[str, bool]:
        """
        Saves or updates a user record based on their OIDC ID or Email.
        Returns (user_id, linked_flag)
        """
        try:
            # 1. Check if a user with this OIDC ID already exists
            user_by_oidc = db.query(models.User).filter(models.User.oidc_id == oidc_id).first()

            if user_by_oidc:
                user_by_oidc.email = email
                user_by_oidc.username = username
                if full_name:
                    user_by_oidc.full_name = full_name
                user_by_oidc.last_login_at = datetime.utcnow()
                
                # Check if user should be promoted to admin based on config
                from app.config import settings
                if email in settings.SUPER_ADMINS and user_by_oidc.role != "admin":
                    user_by_oidc.role = "admin"
                
                db.commit()
                return user_by_oidc.id, False

            # 2. Check if a user with this email already exists (Local -> OIDC Linking)
            user_by_email = db.query(models.User).filter(models.User.email == email).first()
            if user_by_email:
                # Link the OIDC ID to the existing local account
                user_by_email.oidc_id = oidc_id
                user_by_email.username = username # Prefer OIDC preferred_username
                if full_name:
                    user_by_email.full_name = full_name
                user_by_email.last_login_at = datetime.utcnow()
                
                db.commit()
                print(f"[Day 2] Linked OIDC identity {oidc_id} to existing account {email}")
                return user_by_email.id, True

            # 3. Create a new user record
            default_group = self.get_or_create_default_group(db)
            from app.config import settings
            role = "admin" if email in settings.SUPER_ADMINS else "user"

            new_user = models.User(
                id=str(uuid.uuid4()),
                oidc_id=oidc_id,
                email=email,
                username=username,
                full_name=full_name,
                role=role,
                group_id=default_group.id,
                created_at=datetime.utcnow(),
                last_login_at=datetime.utcnow()
            )
            db.add(new_user)
            db.commit()
            db.refresh(new_user)
            return new_user.id, False

        except SQLAlchemyError as e:
            db.rollback()
            raise
        
    def get_user_by_id(self, db: Session, user_id: str) -> Optional[models.User]:
        """
        Retrieves a user record by their unique ID.
        Returns the User object if found, otherwise None.
        """
        try:
            # Query the database for a user with the given ID
            user = db.query(models.User).filter(models.User.id == user_id).first()
            return user
        except SQLAlchemyError as e:
            # Log the error and return None in case of a database issue.
            print(f"Database error while fetching user by ID: {e}")
            return None

    def get_all_users(self, db: Session) -> list[models.User]:
        """Retrieves all registered users."""
        try:
            return db.query(models.User).all()
        except SQLAlchemyError as e:
            print(f"Database error while fetching all users: {e}")
            return []

    def update_user_role(self, db: Session, user_id: str, new_role: str) -> bool:
        """Updates a user's role. Ensures at least one admin exists."""
        try:
            user = db.query(models.User).filter(models.User.id == user_id).first()
            if not user:
                return False
            
            # If trying to demote an admin, check if they are the only one
            if user.role == "admin" and new_role != "admin":
                admin_count = db.query(models.User).filter(models.User.role == "admin").count()
                if admin_count <= 1:
                    # Cannot demote the last admin
                    return False
            
            user.role = new_role
            db.commit()
            return True
        except SQLAlchemyError as e:
            db.rollback()
            print(f"Database error while updating user role: {e}")
            return False

    def get_system_settings(self, db: Session) -> dict:
        """
        Retrieves global AI provider settings.

        Merge strategy controlled by settings.CONFIG_OVERRIDE:
          - False (default): DB admin preferences win. Config.yaml/env only fill missing fields.
          - True:  Config.yaml/env always wins, ignoring any values in the DB.
        """
        from app.config import settings

        if settings.CONFIG_OVERRIDE:
            # Build system settings directly from config.yaml / env — DB is ignored entirely.
            config_prefs = {
                "llm": {
                    "active_provider": settings.ACTIVE_LLM_PROVIDER,
                    "providers": {
                        k: dict(v) for k, v in settings.LLM_PROVIDERS.items()
                    }
                }
            }
            return config_prefs

        # Default: DB admin preferences are authoritative (search for any user with 'admin' role).
        try:
            admin_user = db.query(models.User).filter(models.User.role == "admin").order_by(models.User.created_at).first()
            if admin_user and admin_user.preferences and admin_user.preferences.get("llm"):
                return admin_user.preferences
            
            # Fallback to system-level defaults from config.yaml/env
            return {
                "llm": {
                    "active_provider": settings.ACTIVE_LLM_PROVIDER,
                    "providers": {k: dict(v) for k, v in settings.LLM_PROVIDERS.items()}
                },
                "tts": {
                    "active_provider": settings.TTS_PROVIDER,
                    "providers": {k: dict(v) for k, v in settings.TTS_PROVIDERS.items()}
                },
                "stt": {
                    "active_provider": settings.STT_PROVIDER,
                    "providers": {k: dict(v) for k, v in settings.STT_PROVIDERS.items()}
                }
            }
        except Exception as e:
            print(f"Error fetching system settings: {e}")
            return {}

    # --- Group Management Methods ---

    def get_or_create_default_group(self, db: Session) -> models.Group:
        """Ensures the 'ungrouped' default group exists."""
        default_group = db.query(models.Group).filter(models.Group.id == "ungrouped").first()
        if not default_group:
            default_group = models.Group(
                id="ungrouped",
                name="Ungrouped",
                description="Default group for new users",
                policy={}
            )
            db.add(default_group)
            db.commit()
            db.refresh(default_group)
        return default_group

    def get_all_groups(self, db: Session) -> list[models.Group]:
        """Returns all existing groups, without lazy-loading user relationships."""
        from sqlalchemy.orm import noload
        return db.query(models.Group).options(noload(models.Group.users)).all()

    def get_group_by_id(self, db: Session, group_id: str) -> Optional[models.Group]:
        """Fetches a group by ID, without lazy-loading user relationships."""
        from sqlalchemy.orm import noload
        return db.query(models.Group).options(noload(models.Group.users)).filter(models.Group.id == group_id).first()

    def create_group(self, db: Session, name: str, description: str = None, policy: dict = None) -> Optional[models.Group]:
        """Creates a new user group. Returns None if a group with the same name already exists."""
        existing = db.query(models.Group).filter(
            models.Group.name.ilike(name.strip())
        ).first()
        if existing:
            return None  # Signals a name conflict
        group = models.Group(
            id=str(uuid.uuid4()),
            name=name.strip(),
            description=description,
            policy=policy or {}
        )
        db.add(group)
        db.commit()
        db.refresh(group)
        return group

    def update_group(self, db: Session, group_id: str, name: str = None, description: str = None, policy: dict = None) -> Optional[models.Group]:
        """Updates group metadata or policy. Returns False (bool) if name conflicts with another group."""
        group = self.get_group_by_id(db, group_id)
        if not group:
            return None
        if name:
            name = name.strip()
            # Check for name conflict with a DIFFERENT group
            conflict = db.query(models.Group).filter(
                models.Group.name.ilike(name),
                models.Group.id != group_id
            ).first()
            if conflict:
                return False  # Signals a name conflict (distinct from None = not found)
            group.name = name
        if description is not None: group.description = description
        if policy is not None: group.policy = policy
        db.commit()
        db.refresh(group)
        return group

    def delete_group(self, db: Session, group_id: str) -> bool:
        """Deletes a group. Moves its users to 'ungrouped'."""
        if group_id == "ungrouped":
            return False # Cannot delete the default group
        group = self.get_group_by_id(db, group_id)
        if not group:
            return False
        
        default_group = self.get_or_create_default_group(db)
        # Move users
        db.query(models.User).filter(models.User.group_id == group_id).update({"group_id": default_group.id})
        
        db.delete(group)
        db.commit()
        return True

    def assign_user_to_group(self, db: Session, user_id: str, group_id: str) -> bool:
        """Assigns a user to a group."""
        user = self.get_user_by_id(db, user_id)
        group = self.get_group_by_id(db, group_id)
        if not user or not group:
            return False
        user.group_id = group_id
        db.commit()
        return True

# --- Framework-dependent helper functions ---
# These functions are placeholders and would need to be integrated with your
# specific web framework (e.g., FastAPI, Flask, Django).

def login_required(f):
    """
    A decorator to protect API endpoints and web pages.
    It ensures a user is logged in and is a registered (non-anonymous) user.
    If not, it redirects them to the login page.
    This is a generic implementation. You would replace the logic inside
    with code specific to your web framework's authentication system.
    """
    async def wrapper(*args, **kwargs):
        # Placeholder logic: Check for user in the request context
        # For example, in FastAPI, you might use Depends(get_current_user)
        # In Flask, you might use session or current_user
        user_id = kwargs.get("user_id")  # Assuming user_id is passed in via a dependency
        if not user_id:
            # Depending on the framework, this would return an Unauthorized error or a redirect
            # For example: raise HTTPException(status_code=401, detail="Unauthorized")
            pass
        return await f(*args, **kwargs)
    return wrapper


def get_current_user_id() -> Optional[str]:
    """
    A helper function to get the current user's ID from the session.
    This is a placeholder and needs to be implemented with your framework's
    session/authentication management system.
    """
    # Placeholder logic
    # Example for FastAPI:
    # from fastapi import Depends, Request
    # from app.auth import get_current_user
    #
    # return get_current_user(request).id

    # For now, we return None as a generic placeholder
    return None
