use std::convert::Infallible;
use std::io;
use std::io::Cursor;
use std::path::Path;

use futures_util::StreamExt;
use rustls_acme::acme::LETS_ENCRYPT_PRODUCTION_DIRECTORY;
use rustls_acme::caches::DirCache;
use rustls_acme::{AcmeConfig, AcmeState, CertCache, EventError, EventOk};
use rustls_pemfile::{Item, read_one};
use time::{Duration as TimeDuration, OffsetDateTime};
use x509_parser::parse_x509_certificate;

use crate::runtime_log::{self, AcmeEvent, AcmeRole};

pub(crate) const ACME_TLS_ALPN: &[u8] = b"acme-tls/1";

pub(crate) type ManagedAcmeState = AcmeState<io::Error>;

pub(crate) fn build_acme_state(
    server_hostname: &str,
    email: &str,
    state_directory: &Path,
) -> ManagedAcmeState {
    AcmeConfig::new([server_hostname])
        .contact_push(format!("mailto:{email}"))
        .directory_lets_encrypt(true)
        .cache(DirCache::new(state_directory.to_path_buf()))
        .state()
}

/// Builds an ACME state for the given hostname set.
/// Runewarp reuses the same state directory so the Let's Encrypt account cache can
/// still be shared across independently managed hostnames.
pub(crate) fn build_client_acme_state(
    hostnames: &[String],
    email: &str,
    state_directory: &Path,
) -> ManagedAcmeState {
    AcmeConfig::new(hostnames)
        .contact_push(format!("mailto:{email}"))
        .directory_lets_encrypt(true)
        .cache(DirCache::new(state_directory.to_path_buf()))
        .state()
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub(crate) enum NextDeployment {
    FirstIssuance,
    Renewal,
}

#[derive(Debug)]
pub(crate) struct ManagedAcmeRuntime {
    pub(crate) state: ManagedAcmeState,
    pub(crate) lifecycle: AcmeLifecycle,
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub(crate) enum AcmeLifecycle {
    Server {
        server_hostname: String,
        next_deployment: NextDeployment,
    },
    Client {
        public_hostname: String,
        next_deployment: NextDeployment,
    },
}

#[derive(Clone, Debug, Eq, PartialEq)]
enum CachedCertificateInspection {
    Ready {
        remaining_validity: String,
        renewal_due: bool,
    },
    Missing,
    Expired,
    Unavailable(String),
}

impl AcmeLifecycle {
    pub(crate) async fn server(server_hostname: &str, state_directory: &Path) -> Self {
        let inspection = inspect_cached_certificate(
            &[server_hostname.to_owned()],
            state_directory,
            OffsetDateTime::now_utc(),
        )
        .await;
        emit_startup_inspection(
            std::iter::once(AcmeRole::Server { server_hostname }),
            &inspection,
        );
        Self::Server {
            server_hostname: server_hostname.to_owned(),
            next_deployment: next_deployment_for_inspection(&inspection),
        }
    }

    pub(crate) async fn client(public_hostname: &str, state_directory: &Path) -> Self {
        let inspection = inspect_cached_certificate(
            &[public_hostname.to_owned()],
            state_directory,
            OffsetDateTime::now_utc(),
        )
        .await;
        emit_startup_inspection(
            std::iter::once(AcmeRole::Client { public_hostname }),
            &inspection,
        );
        Self::Client {
            public_hostname: public_hostname.to_owned(),
            next_deployment: next_deployment_for_inspection(&inspection),
        }
    }

    fn handle_ok(&mut self, event: EventOk) {
        match event {
            EventOk::DeployedCachedCert | EventOk::CertCacheStore | EventOk::AccountCacheStore => {}
            EventOk::DeployedNewCert => {
                let acme_event = if matches!(self.next_deployment(), NextDeployment::FirstIssuance)
                {
                    AcmeEvent::CertificateIssued
                } else {
                    AcmeEvent::CertificateRenewed
                };
                self.emit(acme_event);
                self.set_next_deployment(NextDeployment::Renewal);
            }
        }
    }

    fn handle_error(&self, error: &EventError<io::Error, io::Error>) {
        let error = error.to_string();
        self.emit(AcmeEvent::RecoverableFailure { error: &error });
    }

    fn handle_manager_stopped(&self) {
        self.emit(AcmeEvent::ManagerStopped);
    }

    fn emit(&self, event: AcmeEvent<'_>) {
        match self {
            Self::Server {
                server_hostname, ..
            } => runtime_log::acme(
                AcmeRole::Server {
                    server_hostname: server_hostname.as_str(),
                },
                event,
            ),
            Self::Client {
                public_hostname, ..
            } => runtime_log::acme(
                AcmeRole::Client {
                    public_hostname: public_hostname.as_str(),
                },
                event,
            ),
        }
    }

    fn next_deployment(&self) -> NextDeployment {
        match self {
            Self::Server {
                next_deployment, ..
            }
            | Self::Client {
                next_deployment, ..
            } => next_deployment.clone(),
        }
    }

    fn set_next_deployment(&mut self, next: NextDeployment) {
        match self {
            Self::Server {
                next_deployment, ..
            }
            | Self::Client {
                next_deployment, ..
            } => *next_deployment = next,
        }
    }
}

fn next_deployment_for_inspection(inspection: &CachedCertificateInspection) -> NextDeployment {
    match inspection {
        CachedCertificateInspection::Missing => NextDeployment::FirstIssuance,
        CachedCertificateInspection::Ready { .. }
        | CachedCertificateInspection::Expired
        | CachedCertificateInspection::Unavailable(_) => NextDeployment::Renewal,
    }
}

pub(crate) async fn run_acme_state(
    mut state: ManagedAcmeState,
    mut lifecycle: AcmeLifecycle,
) -> io::Result<Infallible> {
    loop {
        match state.next().await {
            Some(Ok(event)) => lifecycle.handle_ok(event),
            Some(Err(error)) => lifecycle.handle_error(&error),
            None => {
                lifecycle.handle_manager_stopped();
                return Err(io::Error::other(
                    "ACME certificate manager stopped unexpectedly",
                ));
            }
        }
    }
}

async fn inspect_cached_certificate(
    domains: &[String],
    state_directory: &Path,
    now: OffsetDateTime,
) -> CachedCertificateInspection {
    let cache = DirCache::new(state_directory.to_path_buf());
    match cache
        .load_cert(domains, LETS_ENCRYPT_PRODUCTION_DIRECTORY)
        .await
    {
        Ok(Some(pem)) => inspect_cached_certificate_pem(&pem, now),
        Ok(None) => CachedCertificateInspection::Missing,
        Err(error) => CachedCertificateInspection::Unavailable(format!("cert cache load: {error}")),
    }
}

fn inspect_cached_certificate_pem(pem: &[u8], now: OffsetDateTime) -> CachedCertificateInspection {
    match parse_cached_certificate_freshness(pem, now) {
        Ok(Some((remaining_validity, renewal_due))) => CachedCertificateInspection::Ready {
            remaining_validity,
            renewal_due,
        },
        Ok(None) => CachedCertificateInspection::Expired,
        Err(error) => CachedCertificateInspection::Unavailable(error),
    }
}

fn parse_cached_certificate_freshness(
    pem: &[u8],
    now: OffsetDateTime,
) -> Result<Option<(String, bool)>, String> {
    let mut reader = Cursor::new(pem);
    while let Some(item) =
        read_one(&mut reader).map_err(|error| format!("cached cert parse: {error}"))?
    {
        let Item::X509Certificate(certificate) = item else {
            continue;
        };
        let (_, certificate) = parse_x509_certificate(certificate.as_ref())
            .map_err(|error| format!("cached cert parse: X509 parsing error: {error}"))?;
        let validity = certificate.validity();
        let not_before = OffsetDateTime::from_unix_timestamp(validity.not_before.timestamp())
            .map_err(|error| format!("cached cert parse: {error}"))?;
        let not_after = OffsetDateTime::from_unix_timestamp(validity.not_after.timestamp())
            .map_err(|error| format!("cached cert parse: {error}"))?;
        let remaining = not_after - now;
        if remaining.is_negative() || remaining.is_zero() {
            return Ok(None);
        }
        let validity_window = not_after - not_before;
        let renewal_due = now >= not_after - validity_window / 3;
        return Ok(Some((format_remaining_validity(remaining), renewal_due)));
    }
    Err("cached cert parse: no certificate PEM found".to_owned())
}

fn format_remaining_validity(remaining: TimeDuration) -> String {
    let days = remaining.whole_days();
    if days >= 1 {
        return format!("{days}d");
    }
    let hours = remaining.whole_hours();
    if hours >= 1 {
        return format!("{hours}h");
    }
    let minutes = remaining.whole_minutes();
    if minutes >= 1 {
        return format!("{minutes}m");
    }
    format!("{}s", remaining.whole_seconds().max(0))
}

fn emit_startup_inspection<'a>(
    roles: impl IntoIterator<Item = AcmeRole<'a>>,
    inspection: &CachedCertificateInspection,
) {
    match inspection {
        CachedCertificateInspection::Ready {
            remaining_validity,
            renewal_due,
        } => {
            for role in roles {
                runtime_log::acme(
                    role,
                    AcmeEvent::CachedCertificateReady {
                        remaining_validity,
                        renewal_due: *renewal_due,
                    },
                );
            }
        }
        CachedCertificateInspection::Missing => {
            for role in roles {
                runtime_log::acme(
                    role,
                    AcmeEvent::FirstIssuanceStarting {
                        reason: "no-ready-cached-certificate",
                    },
                );
            }
        }
        CachedCertificateInspection::Expired => {
            for role in roles {
                runtime_log::acme(
                    role,
                    AcmeEvent::RenewalStarting {
                        reason: "expired-cached-certificate",
                    },
                );
            }
        }
        CachedCertificateInspection::Unavailable(error) => {
            for role in roles {
                runtime_log::acme(role, AcmeEvent::RecoverableFailure { error });
                runtime_log::acme(
                    role,
                    AcmeEvent::RenewalStarting {
                        reason: "unreadable-cached-certificate",
                    },
                );
            }
        }
    }
}

#[cfg(test)]
mod tests {
    use rcgen::{
        CertificateParams, DistinguishedName, DnType, KeyPair, PKCS_ECDSA_P256_SHA256,
        date_time_ymd,
    };

    use super::{
        AcmeLifecycle, CachedCertificateInspection, NextDeployment, inspect_cached_certificate_pem,
        parse_cached_certificate_freshness,
    };

    fn build_cached_certificate_pem(
        not_before: time::OffsetDateTime,
        not_after: time::OffsetDateTime,
    ) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
        let mut params = CertificateParams::new(vec!["app.example.test".to_owned()])?;
        let mut distinguished_name = DistinguishedName::new();
        distinguished_name.push(DnType::CommonName, "app.example.test");
        params.distinguished_name = distinguished_name;
        params.not_before = not_before;
        params.not_after = not_after;
        let key_pair = KeyPair::generate_for(&PKCS_ECDSA_P256_SHA256)?;
        let cert = params.self_signed(&key_pair)?;
        Ok(format!("{}\n{}\n", key_pair.serialize_pem(), cert.pem()).into_bytes())
    }

    #[test]
    fn cached_certificate_freshness_reports_ready_and_not_due_state()
    -> Result<(), Box<dyn std::error::Error>> {
        let pem =
            build_cached_certificate_pem(date_time_ymd(2026, 1, 1), date_time_ymd(2026, 4, 1))?;
        let now = date_time_ymd(2026, 1, 10);

        let inspection = inspect_cached_certificate_pem(&pem, now);

        assert!(matches!(
            inspection,
            CachedCertificateInspection::Ready {
                remaining_validity,
                renewal_due: false,
            } if remaining_validity == "81d"
        ));
        Ok(())
    }

    #[test]
    fn cached_certificate_freshness_reports_renewal_due_state()
    -> Result<(), Box<dyn std::error::Error>> {
        let pem =
            build_cached_certificate_pem(date_time_ymd(2026, 1, 1), date_time_ymd(2026, 4, 1))?;
        let now = date_time_ymd(2026, 3, 10);

        let inspection = inspect_cached_certificate_pem(&pem, now);

        assert!(matches!(
            inspection,
            CachedCertificateInspection::Ready {
                remaining_validity,
                renewal_due: true,
            } if remaining_validity == "22d"
        ));
        Ok(())
    }

    #[test]
    fn cached_certificate_freshness_treats_expired_cache_as_expired()
    -> Result<(), Box<dyn std::error::Error>> {
        let pem =
            build_cached_certificate_pem(date_time_ymd(2026, 1, 1), date_time_ymd(2026, 2, 1))?;
        let now = date_time_ymd(2026, 2, 2);

        let inspection = inspect_cached_certificate_pem(&pem, now);

        assert_eq!(inspection, CachedCertificateInspection::Expired);
        Ok(())
    }

    #[test]
    fn deployed_new_certificate_switches_from_first_issuance_to_renewal() {
        let mut lifecycle = AcmeLifecycle::Server {
            server_hostname: "tunnel.example.test".to_owned(),
            next_deployment: NextDeployment::FirstIssuance,
        };

        lifecycle.handle_ok(rustls_acme::EventOk::DeployedNewCert);

        assert_eq!(
            lifecycle,
            AcmeLifecycle::Server {
                server_hostname: "tunnel.example.test".to_owned(),
                next_deployment: NextDeployment::Renewal,
            }
        );
    }

    #[test]
    fn cached_certificate_parser_rejects_missing_certificates()
    -> Result<(), Box<dyn std::error::Error>> {
        let key_pair = KeyPair::generate_for(&PKCS_ECDSA_P256_SHA256)?;
        let error = parse_cached_certificate_freshness(
            key_pair.serialize_pem().as_bytes(),
            date_time_ymd(2026, 1, 1),
        )
        .unwrap_err();

        assert_eq!(error, "cached cert parse: no certificate PEM found");
        Ok(())
    }
}
