"""Native authentication REST API router."""

import os
from datetime import datetime, timedelta
from typing import List, Optional
from uuid import UUID, uuid4

from fastapi import APIRouter, Depends, HTTPException, Request, status
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
from psycopg import AsyncConnection
from pydantic import BaseModel, EmailStr, Field, field_validator

from fraiseql.auth.native.models import User
from fraiseql.auth.native.tokens import (
    InvalidTokenError,
    SecurityError,
    TokenExpiredError,
    TokenManager,
)


# Pydantic models for request/response
class RegisterRequest(BaseModel):
    email: EmailStr
    password: str = Field(..., min_length=8)
    name: str

    @field_validator("password")
    def validate_password(cls, v):
        if not any(c.isupper() for c in v):
            raise ValueError("Password must contain at least one uppercase letter")
        if not any(c.islower() for c in v):
            raise ValueError("Password must contain at least one lowercase letter")
        if not any(c.isdigit() for c in v):
            raise ValueError("Password must contain at least one digit")
        if not any(c in "!@#$%^&*()_+-=[]{}|;:,.<>?" for c in v):
            raise ValueError("Password must contain at least one special character")
        return v


class LoginRequest(BaseModel):
    email: EmailStr
    password: str


class RefreshRequest(BaseModel):
    refresh_token: str


class LogoutRequest(BaseModel):
    refresh_token: str


class ForgotPasswordRequest(BaseModel):
    email: EmailStr


class ResetPasswordRequest(BaseModel):
    token: str
    new_password: str = Field(..., min_length=8)

    @field_validator("new_password")
    def validate_password(cls, v):
        # Same validation as RegisterRequest
        if not any(c.isupper() for c in v):
            raise ValueError("Password must contain at least one uppercase letter")
        if not any(c.islower() for c in v):
            raise ValueError("Password must contain at least one lowercase letter")
        if not any(c.isdigit() for c in v):
            raise ValueError("Password must contain at least one digit")
        if not any(c in "!@#$%^&*()_+-=[]{}|;:,.<>?" for c in v):
            raise ValueError("Password must contain at least one special character")
        return v


class UserResponse(BaseModel):
    id: UUID
    email: str
    name: str
    roles: List[str]
    permissions: List[str]
    is_active: bool
    email_verified: bool
    created_at: datetime
    updated_at: datetime


class AuthResponse(BaseModel):
    user: UserResponse
    access_token: str
    refresh_token: str
    token_type: str = "bearer"


class TokenResponse(BaseModel):
    access_token: str
    refresh_token: str
    token_type: str = "bearer"


class MessageResponse(BaseModel):
    message: str


class SessionResponse(BaseModel):
    id: UUID
    user_agent: Optional[str]
    ip_address: Optional[str]
    created_at: datetime
    last_used_at: datetime
    is_current: bool = False


# Security
security = HTTPBearer()


# Router
auth_router = APIRouter(tags=["auth"])


# Dependency to get database connection
async def get_db(request: Request) -> AsyncConnection:
    """Get database connection from request."""
    # In a real app, this would come from app state or dependency injection
    # For tests, we'll get it from the request state
    if hasattr(request.app.state, "db_pool"):
        async with request.app.state.db_pool.acquire() as conn:
            yield conn
    else:
        # For testing, we expect the connection to be injected
        yield request.state.db_connection


# Dependency to get schema (for multi-tenant support)
async def get_schema(request: Request) -> str:
    """Get database schema from request."""
    # In production, this might come from tenant context
    # For tests, we use the test schema
    if hasattr(request.app.state, "test_schema"):
        return request.app.state.test_schema
    return "public"  # Default schema


# Dependency to get token manager
def get_token_manager() -> TokenManager:
    """Get token manager instance."""
    # In production, this would use a real secret from config
    secret_key = os.environ.get("JWT_SECRET_KEY", "test-secret-key-change-in-production")
    return TokenManager(secret_key=secret_key)


# Dependency to get current user
async def get_current_user(
    credentials: HTTPAuthorizationCredentials = Depends(security),
    db: AsyncConnection = Depends(get_db),
    schema: str = Depends(get_schema),
    token_manager: TokenManager = Depends(get_token_manager),
) -> User:
    """Get current authenticated user."""
    try:
        payload = token_manager.verify_access_token(credentials.credentials)

        async with db.cursor() as cursor:
            user = await User.get_by_id(cursor, schema, UUID(payload["sub"]))
        if not user:
            raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="User not found")

        if not user.is_active:
            raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Account is disabled")

        # Store session_id for later use
        user._session_id = payload.get("session_id")
        return user

    except (TokenExpiredError, InvalidTokenError, SecurityError) as e:
        raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=str(e))


@auth_router.post("/register", response_model=AuthResponse, status_code=status.HTTP_201_CREATED)
async def register(
    request: RegisterRequest,
    db: AsyncConnection = Depends(get_db),
    schema: str = Depends(get_schema),
):
    """Register a new user."""
    # Check if email already exists
    async with db.cursor() as cursor:
        existing_user = await User.get_by_email(cursor, schema, request.email)
    if existing_user:
        raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="Email already registered")

    # Create new user
    user = User(
        email=request.email,
        password=request.password,
        name=request.name,
        roles=["user"],
        is_active=True,
        email_verified=False,  # Email verification can be added later
    )
    async with db.cursor() as cursor:
        await user.save(cursor, schema)

    # Generate tokens
    token_manager = get_token_manager()
    tokens = token_manager.generate_tokens(str(user.id))
    access_token = tokens["access_token"]
    refresh_token = tokens["refresh_token"]

    # Store session info
    async with db.cursor() as cursor:
        await cursor.execute(
            f"""
            INSERT INTO {schema}.tb_session (fk_user, token_family, device_info, ip_address)
            VALUES (%s, %s, %s, %s)
            RETURNING id
            """,
            (user.id, tokens["family_id"], None, None),
        )

    return AuthResponse(
        user=UserResponse(
            id=user.id,
            email=user.email,
            name=user.name,
            roles=user.roles,
            permissions=user.permissions,
            is_active=user.is_active,
            email_verified=user.email_verified,
            created_at=user.created_at,
            updated_at=user.updated_at,
        ),
        access_token=access_token,
        refresh_token=refresh_token,
    )


@auth_router.post("/login", response_model=AuthResponse)
async def login(
    request: LoginRequest, db: AsyncConnection = Depends(get_db), schema: str = Depends(get_schema)
):
    """Login with email and password."""
    # Get user by email
    async with db.cursor() as cursor:
        user = await User.get_by_email(cursor, schema, request.email)
    if not user or not user.verify_password(request.password):
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid email or password"
        )

    if not user.is_active:
        raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Account is disabled")

    # Generate tokens
    token_manager = get_token_manager()
    tokens = token_manager.generate_tokens(str(user.id))
    access_token = tokens["access_token"]
    refresh_token = tokens["refresh_token"]

    # Store session info
    async with db.cursor() as cursor:
        await cursor.execute(
            f"""
            INSERT INTO {schema}.tb_session (fk_user, token_family, device_info, ip_address)
            VALUES (%s, %s, %s, %s)
            RETURNING id
            """,
            (user.id, tokens["family_id"], None, None),
        )

    return AuthResponse(
        user=UserResponse(
            id=user.id,
            email=user.email,
            name=user.name,
            roles=user.roles,
            permissions=user.permissions,
            is_active=user.is_active,
            email_verified=user.email_verified,
            created_at=user.created_at,
            updated_at=user.updated_at,
        ),
        access_token=access_token,
        refresh_token=refresh_token,
    )


@auth_router.post("/refresh", response_model=TokenResponse)
async def refresh_token(
    request: RefreshRequest,
    db: AsyncConnection = Depends(get_db),
    schema: str = Depends(get_schema),
    token_manager: TokenManager = Depends(get_token_manager),
):
    """Refresh access token using refresh token."""
    try:
        # Verify the refresh token
        payload = token_manager.verify_refresh_token(request.refresh_token)

        # Check if token has been used before (token theft detection)
        async with db.cursor() as cursor:
            await cursor.execute(
                f"""
                SELECT 1 FROM {schema}.tb_used_refresh_token 
                WHERE token = %s
                """,
                (request.refresh_token,),
            )
            if await cursor.fetchone():
                # Token reuse detected - invalidate entire family
                await cursor.execute(
                    f"""
                    UPDATE {schema}.tb_session
                    SET revoked_at = NOW()
                    WHERE token_family = %s
                    """,
                    (payload["family"],),
                )
                raise HTTPException(
                    status_code=status.HTTP_401_UNAUTHORIZED,
                    detail="Token theft detected - all sessions revoked",
                )

            # Mark token as used
            await cursor.execute(
                f"""
                INSERT INTO {schema}.tb_used_refresh_token (token, used_at)
                VALUES (%s, NOW())
                """,
                (request.refresh_token,),
            )

            # Generate new tokens
            new_tokens = token_manager.generate_tokens(
                user_id=payload["sub"], family_id=payload["family"]
            )

            return TokenResponse(
                access_token=new_tokens["access_token"], refresh_token=new_tokens["refresh_token"]
            )

    except (TokenExpiredError, InvalidTokenError, SecurityError) as e:
        raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=str(e))


@auth_router.get("/me", response_model=UserResponse)
async def get_current_user_info(current_user: User = Depends(get_current_user)):
    """Get current user information."""
    return UserResponse(
        id=current_user.id,
        email=current_user.email,
        name=current_user.name,
        roles=current_user.roles,
        permissions=current_user.permissions,
        is_active=current_user.is_active,
        email_verified=current_user.email_verified,
        created_at=current_user.created_at,
        updated_at=current_user.updated_at,
    )


@auth_router.post("/logout", response_model=MessageResponse)
async def logout(
    request: LogoutRequest,
    current_user: User = Depends(get_current_user),
    db: AsyncConnection = Depends(get_db),
    schema: str = Depends(get_schema),
    token_manager: TokenManager = Depends(get_token_manager),
):
    """Logout and invalidate refresh token."""
    # Invalidate the refresh token family
    try:
        payload = token_manager.verify_refresh_token(request.refresh_token)

        async with db.cursor() as cursor:
            await cursor.execute(
                f"""
                UPDATE {schema}.tb_session
                SET revoked_at = NOW()
                WHERE token_family = %s
                """,
                (payload["family"],),
            )

        return MessageResponse(message="Successfully logged out")

    except (TokenExpiredError, InvalidTokenError, SecurityError):
        # Even if token is invalid, return success
        return MessageResponse(message="Successfully logged out")


@auth_router.post("/forgot-password", response_model=MessageResponse)
async def forgot_password(
    request: ForgotPasswordRequest,
    db: AsyncConnection = Depends(get_db),
    schema: str = Depends(get_schema),
):
    """Request password reset email."""
    # Always return success to prevent email enumeration
    async with db.cursor() as cursor:
        user = await User.get_by_email(cursor, schema, request.email)

    if user and user.is_active:
        # Create reset token
        reset_token = str(uuid4())
        expires_at = datetime.utcnow() + timedelta(hours=1)

        await cursor.execute(
            f"""
            INSERT INTO {schema}.tb_password_reset (user_id, token, expires_at)
            VALUES (%s, %s, %s)
            """,
            (user.id, reset_token, expires_at),
        )

        # In a real app, send email here
        # await send_password_reset_email(user.email, reset_token)

    return MessageResponse(message="If the email exists, a reset link has been sent")


@auth_router.post("/reset-password", response_model=MessageResponse)
async def reset_password(
    request: ResetPasswordRequest,
    db: AsyncConnection = Depends(get_db),
    schema: str = Depends(get_schema),
):
    """Reset password using reset token."""
    # Verify reset token
    async with db.cursor() as cursor:
        await cursor.execute(
            f"""
            SELECT pr.*, u.id as user_id
            FROM {schema}.tb_password_reset pr
            JOIN {schema}.tb_user u ON pr.user_id = u.id
            WHERE pr.token = %s
            AND pr.expires_at > NOW()
            AND pr.used = FALSE
            AND u.is_active = TRUE
            """,
            (request.token,),
        )
        result = await cursor.fetchone()

    if not result:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST, detail="Invalid or expired reset token"
        )

    # Update password
    user = await User.get_by_id(cursor, schema, result["user_id"])
    user.set_password(request.new_password)
    await user.update(cursor, schema)

    # Mark token as used
    await cursor.execute(
        f"""
        UPDATE {schema}.tb_password_reset
        SET used = TRUE, used_at = NOW()
        WHERE token = %s
        """,
        (request.token,),
    )

    # Invalidate all sessions for security
    await cursor.execute(
        f"""
        UPDATE {schema}.tb_session
        SET revoked_at = NOW()
        WHERE user_id = %s
        """,
        (user.id,),
    )

    return MessageResponse(message="Password reset successfully")


@auth_router.get("/sessions", response_model=List[SessionResponse])
async def list_sessions(
    current_user: User = Depends(get_current_user),
    db: AsyncConnection = Depends(get_db),
    schema: str = Depends(get_schema),
):
    """List all active sessions for current user."""
    async with db.cursor() as cursor:
        await cursor.execute(
            f"""
            SELECT id, user_agent, ip_address, created_at, last_used_at
            FROM {schema}.tb_session
            WHERE user_id = %s
            AND revoked_at IS NULL
            ORDER BY last_used_at DESC
            """,
            (current_user.id,),
        )
        sessions = await cursor.fetchall()

    # Get current session ID from token
    current_session_id = getattr(current_user, "_session_id", None)

    return [
        SessionResponse(
            id=session["id"],
            user_agent=session["user_agent"],
            ip_address=session["ip_address"],
            created_at=session["created_at"],
            last_used_at=session["last_used_at"],
            is_current=str(session["id"]) == current_session_id,
        )
        for session in sessions
    ]


@auth_router.delete("/sessions/{session_id}", response_model=MessageResponse)
async def revoke_session(
    session_id: UUID,
    current_user: User = Depends(get_current_user),
    db: AsyncConnection = Depends(get_db),
    schema: str = Depends(get_schema),
):
    """Revoke a specific session."""
    # Verify session belongs to user
    async with db.cursor() as cursor:
        await cursor.execute(
            f"""
            SELECT token_family
            FROM {schema}.tb_session
            WHERE id = %s AND user_id = %s
            """,
            (session_id, current_user.id),
        )
        result = await cursor.fetchone()

    if not result:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Session not found")

    # Invalidate the session's token family
    await cursor.execute(
        f"""
        UPDATE {schema}.tb_session
        SET revoked_at = NOW()
        WHERE token_family = %s
        """,
        (result["token_family"],),
    )

    return MessageResponse(message="Session revoked successfully")
