// This file is part of Ortie, a CLI to manage OAuth tokens.
//
// Copyright (C) 2025-2026 Clément DOUIN <pimalaya.org@posteo.net>
//
// This program is free software: you can redistribute it and/or
// modify it under the terms of the GNU Affero General Public License
// as published by the Free Software Foundation, either version 3 of
// the License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful, but
// WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public
// License along with this program. If not, see
// <https://www.gnu.org/licenses/>.

use std::{borrow::Cow, time::Duration};

use anyhow::{anyhow, bail, Result};
use base64::{prelude::BASE64_URL_SAFE_NO_PAD, Engine};
use clap::Parser;
use http::{
    header::{AUTHORIZATION, HOST},
    Request,
};
use humantime::format_duration;
use io_oauth::v2_0::authorization_code_grant::{
    access_token_request::{
        AccessTokenRequestParams, RequestOauth2AccessToken, RequestOauth2AccessTokenResult,
    },
    authorization_response::AuthorizeParams,
    pkce::PkceCodeVerifier,
    state::State,
};
use io_stream::runtimes::std::handle;
use log::debug;
use pimalaya_toolbox::{
    stream::Stream,
    terminal::printer::{Message, Printer},
};
use secrecy::ExposeSecret;
use serde::{
    de::value::{Error, StrDeserializer},
    Deserialize, Serializer,
};
use url::Url;

use crate::account::Account;

/// Resume an existing OAuth 2.0 Authorization Code Grant flow.
///
/// This command can be useful when you initiated an authorization
/// flow in a non-interactive way. After being redirected, this
/// command allows you to complete the authorization flow and get an
/// access and a refresh token from the redirect URI, the initial
/// state as well as the PKCE code verifier.
#[derive(Debug, Parser)]
pub struct ResumeAuthorizationCommand {
    /// The URI the user has been redirected to after opening the
    /// authorization URI.
    ///
    /// This URI is not the user redirect URI, but is the redirect URI
    /// generated by the OAuth 2.0 server. When using the `auth get`
    /// command in an interactive shell, a fake server is spawned in
    /// order to catch this URI.
    #[arg(value_parser = uri_parser)]
    pub redirected_uri: Url,

    /// The state generated during the authorization flow initiation.
    ///
    /// If a state has been generated during the `auth get` command,
    /// it should be given to the `resume` command and should match.
    #[arg(long, short, value_parser = state_parser)]
    #[arg(value_name = "VALUE")]
    pub state: Option<State>,

    /// The PKCE code verifier generated during the authorization flow
    /// initiation.
    ///
    /// If PKCE has been enabled during the `auth get` command, the
    /// generated code should be given to the `resume` command.
    #[arg(long, short, value_parser = pkce_code_verifier_parser)]
    #[arg(value_name = "CODE")]
    pub pkce: Option<PkceCodeVerifier>,

    /// The PKCE code verifier generated during the authorization flow
    /// initiation.
    ///
    /// If PKCE has been enabled during the `auth get` command, the
    /// generated code should be given to the `resume` command.
    #[arg(long, short, value_parser = uri_parser)]
    pub redirect_uri: Option<Url>,
}

impl ResumeAuthorizationCommand {
    pub fn execute(self, printer: &mut impl Printer, account: Account) -> Result<()> {
        let params = match AuthorizeParams::from(&self.redirected_uri) {
            AuthorizeParams::Success(params) => params,
            AuthorizeParams::Error(params) => {
                let err = anyhow!("Authorization error (code {:?})", params.error);
                return Err(match (params.error_description, params.error_uri) {
                    (None, None) => err,
                    (Some(desc), None) => anyhow!("{desc}").context(err),
                    (None, Some(uri)) => anyhow!("{uri}").context(err),
                    (Some(desc), Some(uri)) => anyhow!("{desc}: {uri}").context(err),
                });
            }
        };

        let state = self.state.as_ref().map(Cow::Borrowed);

        if params.state != state {
            let req = self.state.as_ref().map(|state| state.expose());
            let res = params.state.as_ref().map(|state| state.expose());

            let err = anyhow!("Request state {req:?} differs from response state {res:?}")
                .context("Authorization request and response states do not match");

            return Err(err);
        };

        let token_endpoint = &account.endpoints.token;
        let scheme = account.endpoints.token.scheme();

        let Some(host) = account.endpoints.token.host_str() else {
            bail!("Missing token endpoint host name in {token_endpoint}");
        };

        let Some(port) = account.endpoints.token.port_or_known_default() else {
            bail!("Missing token endpoint port in {token_endpoint}");
        };

        let Ok(uri) = format!("{scheme}://{host}:{port}").parse() else {
            bail!("Invalid token URI using {scheme}, {host} and {port}");
        };

        let mut stream = Stream::connect(&uri, &account.tls)?;

        let mut request =
            Request::post(account.endpoints.token.path()).header(HOST, format!("{host}:{port}"));

        if let Some(secret) = account.client_secret {
            let secret = secret.get()?;
            let creds = format!("{}:{}", account.client_id, secret.expose_secret());
            let digest = BASE64_URL_SAFE_NO_PAD.encode(creds);
            request = request.header(AUTHORIZATION, format!("Basic {digest}"));
        }

        let mut params = AccessTokenRequestParams {
            code: params.code,
            redirect_uri: self.redirect_uri.as_ref().map(|uri| uri.as_str().into()),
            client_id: account.client_id.into(),
            pkce_code_verifier: self.pkce.as_ref().map(Cow::Borrowed),
        };

        if let Some(uri) = &account.endpoints.redirection {
            params.redirect_uri = Some(uri.as_str().into());
        }

        let mut send = RequestOauth2AccessToken::new(request, params)?;
        let mut arg = None;

        let res = loop {
            match send.resume(arg.take()) {
                RequestOauth2AccessTokenResult::Ok(res) => break res,
                RequestOauth2AccessTokenResult::Io(io) => arg = Some(handle(&mut stream, io)?),
                RequestOauth2AccessTokenResult::Err(err2) => {
                    let err = "Request OAuth 2.0 access token error";
                    return Err(anyhow!("{err2}").context(err));
                }
            }
        };

        match res {
            Ok(res) => {
                account.storage.write(&res)?;

                debug!("execute issue access token success hook");
                account.hooks.on_issue.execute_success(&res);

                let msg = "Access token successfully issued";
                let msg = match res.expires_in {
                    None => "{msg} (unknown expiry)".into(),
                    Some(exp) => {
                        let exp = Duration::from_secs(exp as u64 + 1);
                        format!("{msg} (expires in {})", format_duration(exp))
                    }
                };

                printer.out(Message::new(msg))
            }
            Err(res) => {
                debug!("execute issue access token error hook");
                account.hooks.on_issue.execute_error(&res);

                let err = anyhow!("Issue access token error (code {:?})", res.error);
                return Err(match (res.error_description, res.error_uri) {
                    (None, None) => err,
                    (Some(desc), None) => anyhow!("{desc}").context(err),
                    (None, Some(uri)) => anyhow!("{uri}").context(err),
                    (Some(desc), Some(uri)) => anyhow!("{desc}: {uri}").context(err),
                });
            }
        }
    }
}

pub fn serialize_state<S: Serializer>(state: &State, s: S) -> Result<S::Ok, S::Error> {
    let state = String::from_utf8_lossy(state.expose());
    s.serialize_str(&state)
}

pub fn serialize_pkce_code_verifier<S: Serializer>(
    verifier: &Option<PkceCodeVerifier>,
    s: S,
) -> Result<S::Ok, S::Error> {
    match verifier {
        Some(verifier) => {
            let verifier = String::from_utf8_lossy(verifier.expose());
            s.serialize_str(&verifier)
        }
        None => s.serialize_none(),
    }
}

pub fn uri_parser(url: &str) -> Result<Url, String> {
    Url::parse(url).map_err(|err| err.to_string())
}

pub fn state_parser(state: &str) -> Result<State, String> {
    match State::deserialize(StrDeserializer::<Error>::new(state)) {
        Ok(state) => Ok(state),
        Err(err) => Err(err.to_string()),
    }
}

pub fn pkce_code_verifier_parser(verifier: &str) -> Result<PkceCodeVerifier, String> {
    match verifier.parse() {
        Ok(verifier) => Ok(verifier),
        Err(b) => {
            let err = format!("Invalid 0x{b:x} found in PKCE code verifier: {verifier}");
            Err(err)
        }
    }
}
