use axum::{
    extract::Query,
    response::{IntoResponse, Redirect},
    routing::get,
    Router,
};
use base64::Engine;
use indieweb::standards::indieauth::{
    AuthorizationRequestFields, Client, CommonRedemptionFields, RedemptionFields,
    RedemptionResponse,
};
use rand::{rngs::OsRng, TryRngCore};
use secrecy::{ExposeSecret, SecretString};
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use std::net::SocketAddr;
use std::sync::Arc;
use tokio::sync::oneshot;
use url::Url;

use crate::config::IndieAuthConfig;
use crate::error::{CliError, Result};
use crate::token::TokenStore;

type HttpClient = indieweb::http::reqwest::Client;
type IndieAuthClient = Client<HttpClient>;

#[derive(Debug, Clone, Deserialize)]
struct CallbackParams {
    code: String,
    state: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AuthResult {
    pub access_token: String,
    pub me: String,
    pub scope: Option<String>,
    pub token_type: String,
}

struct SharedState {
    code_verifier: String,
    state: String,
    tx: Option<oneshot::Sender<Result<String>>>,
}

pub async fn authenticate(
    me: &Url,
    config: &IndieAuthConfig,
    scope: &str,
) -> Result<AuthResult> {
    let http_client = HttpClient::default();

    let client_id = config.client_id.as_ref()
        .map(|u| u.as_str())
        .unwrap_or("https://cli.indieweb.org/");

    let indieauth_client: IndieAuthClient = Client::<HttpClient>::builder()
        .id(client_id)
        .client(http_client)
        .build()
        .map_err(|e| CliError::AuthFailed(e.to_string()))?;

    let metadata = indieauth_client
        .obtain_metadata(me)
        .await
        .map_err(|e| CliError::AuthFailed(format!("Failed to obtain metadata: {}", e)))?;

    let redirect_uri = config.redirect_uri.clone()
        .unwrap_or_else(|| "http://localhost:8080/callback".parse().unwrap());

    let (code_verifier, _code_challenge) = generate_pkce_challenge()?;

    let scopes: indieweb::standards::indieauth::Scopes = scope.parse()
        .map_err(|_| CliError::InvalidScope(scope.to_string()))?;

    let state = generate_state()?;

    let auth_request = AuthorizationRequestFields::new(
        &indieauth_client.id,
        &redirect_uri,
        &state,
    ).map_err(|e| CliError::AuthFailed(e.to_string()))?;

    let auth_request = AuthorizationRequestFields {
        scope: scopes,
        ..auth_request
    };

    let auth_url = auth_request.into_authorization_url(
        metadata.authorization_endpoint.clone(),
        vec![("me".to_string(), me.to_string())],
    ).map_err(|e| CliError::AuthFailed(e.to_string()))?;

    let (tx, rx) = oneshot::channel();
    let shared_state = Arc::new(tokio::sync::Mutex::new(SharedState {
        code_verifier,
        state: state.clone(),
        tx: Some(tx),
    }));

    let port = redirect_uri.port().unwrap_or(8080);
    let addr: SocketAddr = ([127, 0, 0, 1], port).into();

    let app = Router::new()
        .route("/callback", get({
            let shared_state = shared_state.clone();
            move |query: Query<CallbackParams>| {
                let shared_state = shared_state.clone();
                async move {
                    let callback = query.0;
                    let state = shared_state.lock().await;

                    if callback.state.as_deref() != Some(&state.state) {
                        if let Some(tx) = shared_state.lock().await.tx.take() {
                            let _ = tx.send(Err(CliError::AuthFailed("State mismatch".to_string())));
                        }
                        return "State mismatch".into_response();
                    }

                    let code = callback.code.clone();

                    if let Some(tx) = shared_state.lock().await.tx.take() {
                        let _ = tx.send(Ok(code));
                    }

                    Redirect::temporary("data:text/html,<h1>Success! You can close this window.</h1>").into_response()
                }
            }
        }));

    println!("Opening browser for authorization...");
    println!("If the browser doesn't open, visit: {}", auth_url);

    open::that(auth_url.as_str())
        .map_err(|e| CliError::AuthFailed(format!("Failed to open browser: {}", e)))?;

    let listener = tokio::net::TcpListener::bind(addr)
        .await
        .map_err(|e| CliError::AuthFailed(format!("Failed to bind to port: {}", e)))?;

    let server = axum::serve(listener, app);
    let code = tokio::select! {
        result = server => {
            result.map_err(|e| CliError::AuthFailed(format!("Server error: {}", e)))?;
            return Err(CliError::AuthCancelled);
        }
        result = rx => {
            result.map_err(|_| CliError::AuthCancelled)??
        }
    };

    let shared_state = shared_state.lock().await;

    let redemption = RedemptionFields {
        code,
        client_id: indieauth_client.id.clone(),
        redirect_uri: redirect_uri.into(),
        verifier: shared_state.code_verifier.clone(),
    };

    let response: RedemptionResponse<CommonRedemptionFields> = indieauth_client
        .redeem(&metadata.token_endpoint, redemption)
        .await
        .map_err(|e| CliError::AuthFailed(format!("Token redemption failed: {}", e)))?;

    match response {
        RedemptionResponse::Claim(claim) => Ok(AuthResult {
            access_token: claim.access_token,
            me: claim.me.to_string(),
            scope: Some(claim.scope.to_string()),
            token_type: "bearer".to_string(),
        }),
        RedemptionResponse::Error(e) => Err(CliError::AuthFailed(format!(
            "{}: {}",
            e.code,
            e.description.unwrap_or_default()
        ))),
    }
}

fn generate_pkce_challenge() -> Result<(String, String)> {
    let mut bytes = [0u8; 32];
    OsRng.try_fill_bytes(&mut bytes)
        .map_err(|e| CliError::AuthFailed(format!("Random generation failed: {:?}", e)))?;
    let verifier = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(&bytes);

    let mut hasher = Sha256::new();
    hasher.update(verifier.as_bytes());
    let hash = hasher.finalize();
    let challenge = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(&hash);

    Ok((verifier, challenge))
}

fn generate_state() -> Result<String> {
    let mut bytes = [0u8; 16];
    OsRng.try_fill_bytes(&mut bytes)
        .map_err(|e| CliError::AuthFailed(format!("Random generation failed: {:?}", e)))?;
    Ok(base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(&bytes))
}

pub async fn revoke_token(
    token: &SecretString,
    me: &Url,
) -> Result<()> {
    let http_client = HttpClient::default();

    let indieauth_client: IndieAuthClient = Client::<HttpClient>::builder()
        .id("https://cli.indieweb.org/")
        .client(http_client)
        .build()
        .map_err(|e| CliError::AuthFailed(e.to_string()))?;

    let metadata = indieauth_client
        .obtain_metadata(me)
        .await
        .map_err(|e| CliError::AuthFailed(format!("Failed to obtain metadata: {}", e)))?;

    let revocation_endpoint = metadata.revocation_endpoint
        .ok_or(CliError::RevocationFailed)?;

    indieauth_client
        .revoke_token(&revocation_endpoint, token.expose_secret(), None)
        .await
        .map_err(|e: indieweb::Error| CliError::AuthFailed(format!("Revocation failed: {}", e)))?;

    Ok(())
}

pub async fn introspect_token(
    token: &SecretString,
    me: &Url,
) -> Result<serde_json::Value> {
    let http_client = HttpClient::default();

    let indieauth_client: IndieAuthClient = Client::<HttpClient>::builder()
        .id("https://cli.indieweb.org/")
        .client(http_client)
        .build()
        .map_err(|e| CliError::AuthFailed(e.to_string()))?;

    let metadata = indieauth_client
        .obtain_metadata(me)
        .await
        .map_err(|e| CliError::AuthFailed(format!("Failed to obtain metadata: {}", e)))?;

    let introspection_endpoint = metadata.introspection_endpoint
        .ok_or_else(|| CliError::AuthFailed("No introspection endpoint available".to_string()))?;

    let response = indieauth_client
        .introspect_token(&introspection_endpoint, token.expose_secret())
        .await
        .map_err(|e: indieweb::Error| CliError::AuthFailed(format!("Introspection failed: {}", e)))?;

    Ok(serde_json::to_value(response).map_err(|e| CliError::AuthFailed(e.to_string()))?)
}

pub fn save_auth_result(result: &AuthResult, store: &TokenStore) -> Result<()> {
    let token = SecretString::new(result.access_token.clone().into_boxed_str());
    store.save(&token)
}
