from my_modules.app.setup import cache, LIMITER
from my_modules.app.constens import API_GROUP
from my_modules.app.logger import logger

from quart import Blueprint, redirect, url_for, session, request, render_template, abort, make_response, current_app

from authlib.jose import JsonWebKey, JoseError, jwt as authlib_jwt
from authlib.integrations.httpx_client import AsyncOAuth2Client
from jwt import InvalidTokenError

import httpx, uuid, os

auth_login_bp = Blueprint('auth_login', __name__)

# OAuth
OIDC_CLIENT_ID = os.getenv('OIDC_CLIENT_ID', '')
OIDC_CLIENT_SECRET = os.getenv('OIDC_CLIENT_SECRET', '')
OIDC_METADATA_URL = os.getenv('OIDC_METADATA_URL', '')
REDIRECT_URI_SCHEME = os.getenv('REDIRECT_URI_SCHEME', 'http')

async def get_oidc_metadata():
  async with httpx.AsyncClient() as client:
    try:
      response = await client.get(OIDC_METADATA_URL)
      response.raise_for_status()
      return response.json()
    except httpx.ReadTimeout:
      return await get_oidc_metadata()

@auth_login_bp.route('/login', methods=['GET'])
@auth_login_bp.route('/auth', methods=['GET'])
@LIMITER.limit("5 per minute; 30 per hour")
async def login():
  metadata = await get_oidc_metadata()

  auth_id = request.cookies.get('auth_id') or str(uuid.uuid4())
  nonce = str(uuid.uuid4())

  await cache.set(f"oauth:nonce:{auth_id}", nonce, ttl=3603)

  client = AsyncOAuth2Client(
    client_id=OIDC_CLIENT_ID,
    client_secret=OIDC_CLIENT_SECRET,
    redirect_uri=url_for('auth_login.auth_callback', _external=True, _scheme=REDIRECT_URI_SCHEME),
    scope='openid profile email',
  )

  uri, state = client.create_authorization_url(
    metadata['authorization_endpoint'],
    nonce=nonce,
    state=auth_id,
  )

  response = await make_response(redirect(uri))
  response.set_cookie('auth_id', auth_id, max_age=3600, httponly=True, secure=True, samesite='Lax')
  return response

@auth_login_bp.route('/logout', methods=['GET'])
@auth_login_bp.route('/auth/logout', methods=['GET'])
@LIMITER.limit("10 per minute")
async def logout():
  user = session.get('user')
  if user:
    await logger.info(f'logging out user: {user}')
    session.pop('user', None)

  session.clear()
  return redirect(url_for('side_main.index'))

@auth_login_bp.route('/auth/callback', methods=['GET'])
@LIMITER.limit("5 per minute")
async def auth_callback():
  try:
    auth_id = request.args.get('state')
    code = request.args.get('code')
    nonce = await cache.get(f"oauth:nonce:{auth_id}")

    if not nonce:
      await logger.error('Nonce not found')
      return await render_template('views/api/token.htm', error='Nonce not found'), 400

    await cache.delete(f"oauth:nonce:{auth_id}")

    metadata = await get_oidc_metadata()
    client = AsyncOAuth2Client(
      client_id=OIDC_CLIENT_ID,
      client_secret=OIDC_CLIENT_SECRET,
      redirect_uri=url_for('auth_login.auth_callback', _external=True, _scheme=REDIRECT_URI_SCHEME),
      scope='openid profile email',
    )

    # Exchange code for token
    token_fetched = False
    while not token_fetched:
      try:
        token = await client.fetch_token(
          metadata['token_endpoint'],
          code=code,
          grant_type='authorization_code'
        )
        await logger.debug(f'Auth Callback | token: {token}')
        token_fetched = True
      except httpx.ReadTimeout:
        pass

    # Decode ID token
    id_token = token.get('id_token')
    if not id_token:
      await logger.error('ID token missing in OAuth response')
      return await render_template('views/api/token.htm', error='ID token missing'), 400

    # Fetch the JWKs to verify signature
    async with httpx.AsyncClient() as http_client:
      jwks_resp = await http_client.get(metadata['jwks_uri'])
      jwks = jwks_resp.json()

    keys = JsonWebKey.import_key_set(jwks)
    claims = authlib_jwt.decode(id_token, key=keys)

    try:
      claims.validate_aud()
      claims.validate()
    except JoseError as e:
      await logger.error(f"JWT validation error: {e}")
      return await render_template('views/api/token.htm', error=str(e)), 400

    if claims.get('nonce') != nonce:
      await logger.error('Nonce mismatch in ID token')
      return await render_template('views/api/token.htm', error='Invalid nonce'), 400

    await logger.info(f'Auth Callback | user_info: {claims}')

    if API_GROUP not in claims.get('groups', []):
      await logger.error("You don't have Permissions to Access this API")
      return await render_template('views/api/token.htm', error="You don't have Permissions to Access this API"), 403

    session['user'] = claims

    response = await make_response(redirect(url_for('side_main.index')))
    response.set_cookie('auth_id', '', max_age=0, httponly=True, secure=True, samesite='Lax')
    return response

  except httpx.HTTPError as e:
    await logger.error(f"HTTP error during token exchange: {e}")
    return await render_template('views/api/token.htm', error="Token exchange failed"), 500
  except InvalidTokenError as e:
    await logger.error(f"Invalid ID Token: {e}")
    return await render_template('views/api/token.htm', error="Invalid ID Token"), 400
  except Exception as e:
    await logger.exception("Unknown error during OAuth callback")
    return await render_template('views/api/token.htm', error=str(e)), 500
