pub mod default_browser;
pub mod default_config;
pub mod default_verifier;
pub mod device_fingerprint;
pub mod jws;
pub mod platform_client;
pub mod public_key_resolver;
pub mod root_keys;

pub use default_browser::DefaultBrowserOpener;
pub use default_config::{
    axis_plugin_home_dir, default_device_id_path, default_license_cache_path,
    resolve_platform_base_url, DEFAULT_AXIS_PLATFORM_BASE_URL, SDK_CLIENT_VERSION,
};
pub use default_verifier::{read_tenant_id_from_license, ResolverBackedVerifier};
pub use device_fingerprint::{get_device_fingerprint, get_device_name};
pub use platform_client::ReqwestPlatformClient;
pub use public_key_resolver::{PublicKeyResolver, PublicKeyResolverConfig, PublicKeyResolverError};
pub use root_keys::{root_key_pem, AXIS_PLATFORM_ROOT_PUBLIC_KEYS};

use std::sync::Arc;

use std::fs;
use std::path::{Path, PathBuf};
use std::thread::sleep;
use std::time::{Duration, SystemTime, UNIX_EPOCH};

const DEFAULT_CACHE_PATH: &str = ".pay-plugin/license.json";
const DEFAULT_MIN_POLL_INTERVAL: Duration = Duration::from_secs(1);

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CheckSource {
    Cache,
    Platform,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum FailureReason {
    UserCancelled,
    NetworkError,
    SessionExpired,
    ProductNotFound,
    ProductInactive,
    PlatformError,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CheckResult {
    Authorized { source: CheckSource },
    Denied { reason: FailureReason },
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SessionStatus {
    Created,
    PendingLogin,
    PendingSubscription,
    PendingPayment,
    Authorized,
    Cancelled,
    Expired,
    Failed,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CreateSessionInput {
    pub product_id: String,
    pub client_type: String,
    pub client_version: String,
    pub device_fingerprint: String,
    pub device_name: Option<String>,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PlatformSession {
    pub auth_session_id: String,
    pub poll_token: String,
    pub login_url: String,
    pub expires_at_epoch_millis: Option<u64>,
    pub poll_interval_seconds: u64,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PollSessionResult {
    pub auth_session_id: String,
    pub status: SessionStatus,
    pub license_token: Option<String>,
    pub reason: Option<String>,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CachedLicense {
    pub license_token: String,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PayPluginError {
    Network,
    Platform(String),
    Storage(String),
}

pub trait PlatformClient {
    fn create_session(
        &self,
        input: &CreateSessionInput,
    ) -> Result<PlatformSession, PayPluginError>;

    fn poll_session(
        &self,
        session: &PlatformSession,
    ) -> Result<PollSessionResult, PayPluginError>;
}

pub trait BrowserOpener {
    fn open(&self, url: &str) -> Result<(), PayPluginError>;
}

pub trait CacheStore {
    fn read(&self, path: &Path) -> Result<Option<CachedLicense>, PayPluginError>;
    fn write(&self, path: &Path, license_token: &str) -> Result<(), PayPluginError>;
}

pub trait LicenseVerifier {
    fn verify(
        &self,
        license_token: &str,
        tenant_id: &str,
        product_id: &str,
        device_fingerprint: &str,
    ) -> bool;
}

#[derive(Debug, Clone)]
pub struct PayPluginConfig {
    pub tenant_id: String,
    pub client_version: String,
    pub device_fingerprint: String,
    pub device_name: Option<String>,
    pub client_type: String,
    pub cache_path: PathBuf,
}

impl PayPluginConfig {
    pub fn rust(
        tenant_id: impl Into<String>,
        client_version: impl Into<String>,
        device_fingerprint: impl Into<String>,
    ) -> Self {
        Self {
            tenant_id: tenant_id.into(),
            client_version: client_version.into(),
            device_fingerprint: device_fingerprint.into(),
            device_name: None,
            client_type: "rust".to_string(),
            cache_path: PathBuf::from(DEFAULT_CACHE_PATH),
        }
    }

    pub fn with_device_name(mut self, device_name: impl Into<String>) -> Self {
        self.device_name = Some(device_name.into());
        self
    }
}

pub struct PayPlugin<C, O, S, V> {
    config: PayPluginConfig,
    client: C,
    opener: O,
    cache: S,
    verifier: V,
}

impl<C, O, S, V> PayPlugin<C, O, S, V>
where
    C: PlatformClient,
    O: BrowserOpener,
    S: CacheStore,
    V: LicenseVerifier,
{
    pub fn new(
        config: PayPluginConfig,
        client: C,
        opener: O,
        cache: S,
        verifier: V,
    ) -> Self {
        Self {
            config,
            client,
            opener,
            cache,
            verifier,
        }
    }

    pub fn check(&self, product_id: &str) -> CheckResult {
        if let Ok(Some(cached)) = self.cache.read(&self.config.cache_path) {
            if self.verifier.verify(
                &cached.license_token,
                &self.config.tenant_id,
                product_id,
                &self.config.device_fingerprint,
            ) {
                return CheckResult::Authorized {
                    source: CheckSource::Cache,
                };
            }
        }

        let session = match self.client.create_session(&CreateSessionInput {
            product_id: product_id.to_string(),
            client_type: self.config.client_type.clone(),
            client_version: self.config.client_version.clone(),
            device_fingerprint: self.config.device_fingerprint.clone(),
            device_name: self.config.device_name.clone(),
        }) {
            Ok(session) => session,
            Err(error) => return deny_from_error(error),
        };

        eprintln!("授权链接: {}", session.login_url);
        eprintln!("如浏览器未自动打开，请手动复制上述链接完成授权。");
        if let Err(error) = self.opener.open(&session.login_url) {
            return deny_from_error(error);
        }

        match wait_for_authorized_session(&self.client, &session) {
            Ok(token) => {
                let _ = self.cache.write(&self.config.cache_path, &token);
                CheckResult::Authorized {
                    source: CheckSource::Platform,
                }
            }
            Err(reason) => CheckResult::Denied { reason },
        }
    }
}

pub struct FileCache;

impl CacheStore for FileCache {
    fn read(&self, path: &Path) -> Result<Option<CachedLicense>, PayPluginError> {
        let raw = match fs::read_to_string(path) {
            Ok(raw) => raw,
            Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(None),
            Err(error) => return Err(PayPluginError::Storage(error.to_string())),
        };

        Ok(extract_json_string(&raw, "license_token").map(|license_token| CachedLicense {
            license_token,
        }))
    }

    fn write(&self, path: &Path, license_token: &str) -> Result<(), PayPluginError> {
        if let Some(parent) = path.parent() {
            fs::create_dir_all(parent).map_err(|error| PayPluginError::Storage(error.to_string()))?;
        }

        let escaped = escape_json_string(license_token);
        let body = format!(
            "{{\"license_token\":\"{}\",\"cached_at_epoch_millis\":{}}}",
            escaped,
            now_epoch_millis()
        );
        fs::write(path, body).map_err(|error| PayPluginError::Storage(error.to_string()))
    }
}

pub struct NoopBrowserOpener;

impl BrowserOpener for NoopBrowserOpener {
    fn open(&self, _url: &str) -> Result<(), PayPluginError> {
        Ok(())
    }
}

pub fn wait_for_authorized_session<C: PlatformClient>(
    client: &C,
    session: &PlatformSession,
) -> Result<String, FailureReason> {
    loop {
        if is_expired(session) {
            return Err(FailureReason::SessionExpired);
        }

        let poll = client
            .poll_session(session)
            .map_err(|error| match error {
                PayPluginError::Network => FailureReason::NetworkError,
                _ => FailureReason::PlatformError,
            })?;

        match poll.status {
            SessionStatus::Authorized => {
                return poll.license_token.ok_or(FailureReason::PlatformError);
            }
            SessionStatus::Expired => return Err(FailureReason::SessionExpired),
            SessionStatus::Cancelled => return Err(FailureReason::UserCancelled),
            SessionStatus::Failed => return Err(FailureReason::PlatformError),
            SessionStatus::Created
            | SessionStatus::PendingLogin
            | SessionStatus::PendingSubscription
            | SessionStatus::PendingPayment => {
                let delay = Duration::from_secs(session.poll_interval_seconds)
                    .max(DEFAULT_MIN_POLL_INTERVAL);
                sleep(delay);
            }
        }
    }
}

fn deny_from_error(error: PayPluginError) -> CheckResult {
    let reason = match error {
        PayPluginError::Network => FailureReason::NetworkError,
        PayPluginError::Platform(message)
            if message == "product_not_found" || message == "product_inactive" =>
        {
            if message == "product_not_found" {
                FailureReason::ProductNotFound
            } else {
                FailureReason::ProductInactive
            }
        }
        PayPluginError::Platform(_) | PayPluginError::Storage(_) => FailureReason::PlatformError,
    };

    CheckResult::Denied { reason }
}

fn is_expired(session: &PlatformSession) -> bool {
    let Some(expires_at) = session.expires_at_epoch_millis else {
        return false;
    };

    now_epoch_millis() >= expires_at
}

fn now_epoch_millis() -> u64 {
    SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .unwrap_or_default()
        .as_millis() as u64
}

fn extract_json_string(raw: &str, key: &str) -> Option<String> {
    let needle = format!("\"{}\"", key);
    let start = raw.find(&needle)?;
    let after_key = &raw[start + needle.len()..];
    let colon = after_key.find(':')?;
    let after_colon = after_key[colon + 1..].trim_start();
    if !after_colon.starts_with('"') {
        return None;
    }

    let mut value = String::new();
    let mut escaped = false;
    for ch in after_colon[1..].chars() {
        if escaped {
            value.push(ch);
            escaped = false;
            continue;
        }
        if ch == '\\' {
            escaped = true;
            continue;
        }
        if ch == '"' {
            return Some(value);
        }
        value.push(ch);
    }

    None
}

fn escape_json_string(value: &str) -> String {
    value.replace('\\', "\\\\").replace('"', "\\\"")
}

/// Zero-config entry point. Plugin integrators call `check(product_id)` and
/// everything (platform URL, device fingerprint, license cache, public-key
/// fetch + JWS verification) is handled internally. Override
/// `AXIS_PAY_PLUGIN_BASE_URL` to point at a self-hosted Axis platform.
pub fn check(product_id: &str) -> CheckResult {
    let base_url = resolve_platform_base_url();
    let resolver = match PublicKeyResolver::new(PublicKeyResolverConfig {
        base_url: base_url.clone(),
        ..PublicKeyResolverConfig::default()
    }) {
        Ok(value) => Arc::new(value),
        Err(_) => return CheckResult::Denied { reason: FailureReason::PlatformError },
    };
    let verifier = ResolverBackedVerifier::new(resolver);
    let platform_client = match ReqwestPlatformClient::new(base_url) {
        Ok(value) => value,
        Err(_) => return CheckResult::Denied { reason: FailureReason::NetworkError },
    };
    let cache_path = default_license_cache_path(product_id);
    let mut config = PayPluginConfig::rust(
        String::new(),
        SDK_CLIENT_VERSION,
        get_device_fingerprint(),
    )
    .with_device_name(get_device_name());
    config.cache_path = cache_path;
    let plugin = PayPlugin::new(
        config,
        platform_client,
        DefaultBrowserOpener,
        FileCache,
        verifier,
    );
    plugin.check(product_id)
}
