import argparse
import sys
import sqlite3
import secrets
from datetime import datetime, timedelta
from typing import Optional, Dict

import jwt
from fastapi import FastAPI, HTTPException, Depends, status
from fastapi.security import OAuth2PasswordRequestForm
from passlib.hash import bcrypt
from pydantic import BaseModel
import uvicorn

# Constants and configurations
JWT_SECRET_KEY = secrets.token_urlsafe(32)
JWT_ALGORITHM = 'HS256'
ACCESS_TOKEN_EXPIRE_MINUTES = 15
REFRESH_TOKEN_EXPIRE_DAYS = 7

DB_NAME = 'users.db'


def initialize_db() -> None:
    """
    Initializes the SQLite database with the users table.
    """
    conn = sqlite3.connect(DB_NAME)
    cursor = conn.cursor()
    cursor.execute('''
        CREATE TABLE IF NOT EXISTS users (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            username TEXT UNIQUE NOT NULL,
            password_hash TEXT NOT NULL,
            role TEXT NOT NULL
        )
    ''')
    conn.commit()
    conn.close()


def get_db_connection() -> sqlite3.Connection:
    """
    Creates and returns a new database connection.
    """
    return sqlite3.connect(DB_NAME)


class User(BaseModel):
    username: str
    role: str


class UserInDB(User):
    password_hash: str


class Token(BaseModel):
    access_token: str
    refresh_token: str
    token_type: str = 'bearer'


class TokenData(BaseModel):
    username: Optional[str] = None
    role: Optional[str] = None


app = FastAPI()


def create_access_token(data: Dict[str, str], expires_delta: Optional[timedelta] = None) -> str:
    """
    Creates a JWT access token.
    """
    to_encode = data.copy()
    expire = datetime.utcnow() + (expires_delta or timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES))
    to_encode.update({"exp": expire})
    encoded_jwt = jwt.encode(to_encode, JWT_SECRET_KEY, algorithm=JWT_ALGORITHM)
    return encoded_jwt


def create_refresh_token(data: Dict[str, str]) -> str:
    """
    Creates a JWT refresh token.
    """
    expire = datetime.utcnow() + timedelta(days=REFRESH_TOKEN_EXPIRE_DAYS)
    to_encode = data.copy()
    to_encode.update({"exp": expire})
    encoded_jwt = jwt.encode(to_encode, JWT_SECRET_KEY, algorithm=JWT_ALGORITHM)
    return encoded_jwt


def verify_password(plain_password: str, hashed_password: str) -> bool:
    """
    Verifies a password against its hash.
    """
    return bcrypt.verify(plain_password, hashed_password)


def get_password_hash(password: str) -> str:
    """
    Hashes a password using bcrypt.
    """
    return bcrypt.hash(password)


def get_user(username: str) -> Optional[UserInDB]:
    """
    Retrieves a user from the database by username.
    """
    conn = get_db_connection()
    cursor = conn.cursor()
    cursor.execute('SELECT username, password_hash, role FROM users WHERE username = ?', (username,))
    row = cursor.fetchone()
    conn.close()
    if row:
        return UserInDB(username=row[0], password_hash=row[1], role=row[2])
    return None


def create_user(username: str, password: str, role: str) -> bool:
    """
    Creates a new user in the database.
    """
    try:
        password_hash = get_password_hash(password)
        conn = get_db_connection()
        cursor = conn.cursor()
        cursor.execute(
            'INSERT INTO users (username, password_hash, role) VALUES (?, ?, ?)',
            (username, password_hash, role)
        )
        conn.commit()
        conn.close()
        return True
    except sqlite3.IntegrityError:
        return False


async def get_current_user(token: str = Depends(lambda: None)) -> UserInDB:
    """
    Dependency to get the current user from the JWT token.
    """
    if token is None:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail='Missing token'
        )
    try:
        payload = jwt.decode(token, JWT_SECRET_KEY, algorithms=[JWT_ALGORITHM])
        username: str = payload.get('sub')
        role: str = payload.get('role')
        if username is None or role is None:
            raise HTTPException(
                status_code=status.HTTP_401_UNAUTHORIZED,
                detail='Invalid token payload'
            )
        user = get_user(username)
        if user is None:
            raise HTTPException(
                status_code=status.HTTP_401_UNAUTHORIZED,
                detail='User not found'
            )
        return user
    except jwt.ExpiredSignatureError:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail='Token expired'
        )
    except jwt.PyJWTError:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail='Invalid token'
        )


def role_required(required_role: str):
    """
    Dependency factory to enforce role-based access.
    """
    async def role_dependency(current_user: UserInDB = Depends(get_current_user)):
        if current_user.role != required_role:
            raise HTTPException(
                status_code=status.HTTP_403_FORBIDDEN,
                detail='Insufficient permissions'
            )
        return current_user
    return role_dependency


@app.post('/signup')
def signup(user: User, password: str, role: str = 'user') -> Dict[str, str]:
    """
    Endpoint to register a new user.
    """
    success = create_user(user.username, password, role)
    if not success:
        raise HTTPException(status_code=400, detail='Username already exists')
    return {'message': 'User created successfully'}


@app.post('/login', response_model=Token)
def login(form_data: OAuth2PasswordRequestForm = Depends()) -> Token:
    """
    Endpoint for user login; returns access and refresh tokens.
    """
    user = get_user(form_data.username)
    if not user or not verify_password(form_data.password, user.password_hash):
        raise HTTPException(status_code=400, detail='Incorrect username or password')
    token_data = {'sub': user.username, 'role': user.role}
    access_token = create_access_token(token_data)
    refresh_token = create_refresh_token(token_data)
    return Token(access_token=access_token, refresh_token=refresh_token)


@app.post('/refresh', response_model=Token)
def refresh_token(refresh_token: str) -> Token:
    """
    Endpoint to refresh tokens.
    """
    try:
        payload = jwt.decode(refresh_token, JWT_SECRET_KEY, algorithms=[JWT_ALGORITHM])
        username: str = payload.get('sub')
        role: str = payload.get('role')
        if username is None or role is None:
            raise HTTPException(status_code=401, detail='Invalid refresh token')
        user = get_user(username)
        if user is None:
            raise HTTPException(status_code=401, detail='User not found')
        token_data = {'sub': user.username, 'role': user.role}
        access_token = create_access_token(token_data)
        new_refresh_token = create_refresh_token(token_data)
        return Token(access_token=access_token, refresh_token=new_refresh_token)
    except jwt.ExpiredSignatureError:
        raise HTTPException(status_code=401, detail='Refresh token expired')
    except jwt.PyJWTError:
        raise HTTPException(status_code=401, detail='Invalid refresh token')


@app.get('/admin')
def admin_endpoint(current_user: UserInDB = Depends(role_required('admin'))):
    """
    Example endpoint protected by role-based access (admin only).
    """
    return {'message': f'Hello, {current_user.username}. You have admin access.'}


@app.get('/user')
def user_endpoint(current_user: UserInDB = Depends(get_current_user)):
    """
    Example endpoint accessible by any logged-in user.
    """
    return {'message': f'Hello, {current_user.username}. Your role is {current_user.role}.'}


def run_server():
    """
    Initializes the database and runs the FastAPI server.
    """
    initialize_db()
    uvicorn.run(app, host='127.0.0.1', port=8000)


def main():
    parser = argparse.ArgumentParser(description='FastAPI JWT Authentication Module')
    parser.add_argument('--run', action='store_true', help='Run the server')
    parser.add_argument('--help', action='help', help='Show this help message and exit')
    args = parser.parse_args()

    if args.run:
        run_server()
    else:
        parser.print_help()


if __name__ == '__main__':
    main()