import asyncio
import pathlib
import typing

import click
import cloup
import httpx

import factorialhr
from factorialhr._cli import access_token
from factorialhr._cli import common as common_cli
from factorialhr._client import ApiClient, RefreshTokenAuth, RefreshTokenAuthFile


async def _print_account_info(
    auth: RefreshTokenAuth,
    url: typing.Literal[
        'https://api.factorialhr.com',
        'https://api.demo.factorial.dev',
    ],
):
    async with ApiClient(auth=auth, base_url=url) as api:
        credentials, _ = await factorialhr.CredentialsEndpoint(api).get()
    if len(credentials) > 1:
        raise ValueError('More than one credentials is currently not supported')
    click.echo(
        f'Logged in as {credentials[0].full_name} on {url}. Session expires at {auth.refresh_token_expiration} '
        f'{auth.created_at.tzname()}.',
    )


def get_session() -> tuple[
    RefreshTokenAuthFile,
    typing.Literal[
        'https://api.factorialhr.com',
        'https://api.demo.factorial.dev',
    ],
]:
    if not SESSION_FILE.exists():
        raise click.ClickException('No session found. Login with "factorialhr account login".')
    auth, url = RefreshTokenAuthFile.from_file(SESSION_FILE)
    if auth.is_refresh_token_expired():
        msg = f'Session expired at {auth.refresh_token_expiration}. Login with "factorialhr account login".'
        raise click.ClickException(msg)
    return auth, url


async def _logout():
    try:
        auth, url = get_session()
    except click.ClickException:
        SESSION_FILE.unlink(missing_ok=True)
        return

    async with httpx.AsyncClient(base_url=url) as client:
        await asyncio.gather(
            client.post(
                'oauth/revoke',
                data={'client_id': auth.client_id, 'client_secret': auth.client_secret, 'token': auth.access_token},
            ),
            client.post(
                'oauth/revoke',
                data={'client_id': auth.client_id, 'client_secret': auth.client_secret, 'token': auth.refresh_token},
            ),
        )
    SESSION_FILE.unlink()


async def _login(  # noqa: PLR0913
    client_id: str,
    client_secret: str,
    url: typing.Literal[
        'https://api.factorialhr.com',
        'https://api.demo.factorial.dev',
    ],
    scope: str,
    redirect_url: str,
    auth_code: str | None,
):
    if auth_code:
        response = await access_token.get_access_token_from_authorization_code(
            authorization_code=auth_code,
            client_id=client_id,
            client_secret=client_secret,
            target_url=url,
            redirect_uri=redirect_url,
        )
    else:
        try:
            response = await access_token.get_access_token(
                client_id=client_id,
                client_secret=client_secret,
                target_url=url,
                redirect_uri=redirect_url,
                scope=scope,
                http_server_timeout=None,
                ssl_context=None,
            )
        except asyncio.CancelledError:
            raise click.ClickException('Operation cancelled')  # noqa: B904
        except RuntimeError as e:
            raise click.ClickException(str(e)) from e
    auth = RefreshTokenAuthFile(
        file=SESSION_FILE,
        refresh_token=response.refresh_token,
        client_id=client_id,
        client_secret=client_secret,
        access_token=response.access_token,
        token_type=response.token_type,
        created_at=response.created_at,
    )
    auth.to_file(url)
    await _print_account_info(auth, url)


@cloup.group(help='Manages an oauth2 session.')
def account():
    pass


APP_DIR = pathlib.Path(click.get_app_dir('factorialhr_api'))
APP_DIR.mkdir(exist_ok=True)
SESSION_FILE = APP_DIR.joinpath('session.json')


@cloup.command(help='Create an oauth2 session.')
@cloup.argument('client_id', type=str)
@cloup.argument('client_secret', type=str)
@cloup.option('--demo', help='Whether to use the production or demo api.', is_flag=True)
@cloup.option('--scope', help='Oauth2 access scope. Defaults to "read".', default='read')
@cloup.option(
    '--redirect-url',
    help='Where to redirect for oauth2 callback',
    default=f'https://redirectmeto.com/http://{access_token.HTTP_SERVER}:{access_token.HTTP_SERVER_PORT}/authorize/authorization_code',
)
@cloup.option('--auth-code', help='Authorization code to use for login.')
@common_cli.to_async
async def login(client_id: str, client_secret: str, demo: bool, scope: str, redirect_url: str, auth_code: str | None):  # noqa: FBT001, PLR0913
    await _login(
        client_id=client_id,
        client_secret=client_secret,
        url='https://api.factorialhr.com' if not demo else 'https://api.demo.factorial.dev',
        scope=scope,
        redirect_url=redirect_url,
        auth_code=auth_code,
    )


@cloup.command(help='Get information about the current session.')
def status():
    auth, _ = get_session()
    click.echo(
        f'Session expires at {auth.refresh_token_expiration} {auth.created_at.tzname()}.',
    )


@cloup.command(help='Refresh an already created session.')
@common_cli.to_async
async def refresh():
    auth, url = get_session()
    await auth.refresh_access_token(url)
    await _print_account_info(auth, url)


@cloup.command(help='Invalidate the session.')
@common_cli.to_async
async def logout():
    await _logout()


account.add_command(login)
account.add_command(status)
account.add_command(refresh)
account.add_command(logout)
