use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::fs;

use crate::device;

/// Telemetry endpoint.
const TELEMETRY_URL: &str = "https://manifold-bt.com/api/telemetry";

/// Local state persisted between runs.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LocalState {
    /// Hash of the device
    pub device_hash: String,
    /// Last successful ping timestamp
    pub last_ping: Option<DateTime<Utc>>,
    /// Whether this device has EVER successfully pinged
    pub ever_connected: bool,
    /// First launch timestamp (to compute 72h grace)
    pub first_launch: DateTime<Utc>,
}

impl LocalState {
    /// Load from disk or create a new state.
    pub fn load_or_create() -> Self {
        let path = device::state_file();
        if let Ok(data) = fs::read_to_string(&path) {
            if let Ok(state) = serde_json::from_str::<LocalState>(&data) {
                return state;
            }
        }
        let state = LocalState {
            device_hash: device::device_hash(),
            last_ping: None,
            ever_connected: false,
            first_launch: Utc::now(),
        };
        let _ = state.save();
        state
    }

    /// Save state to disk.
    pub fn save(&self) -> Result<(), std::io::Error> {
        let dir = device::license_dir();
        fs::create_dir_all(&dir)?;
        let json = serde_json::to_string_pretty(self).map_err(std::io::Error::other)?;
        fs::write(device::state_file(), json)
    }

    /// How many hours since first launch.
    pub fn hours_since_first_launch(&self) -> i64 {
        (Utc::now() - self.first_launch).num_hours()
    }

    /// Has the 72h offline grace period expired?
    pub fn grace_period_expired(&self) -> bool {
        !self.ever_connected && self.hours_since_first_launch() > 72
    }
}

/// Telemetry payload sent to the server.
#[derive(Debug, Serialize)]
struct PingPayload {
    license_hash: String,
    license_id: String,
    email: String,
    device_hash: String,
    timestamp: DateTime<Utc>,
    version: String,
}

/// Server response from the telemetry endpoint.
#[derive(Debug, Deserialize)]
struct PingResponse {
    ok: bool,
    #[serde(default)]
    licensed: Option<bool>,
}

/// Send a background ping. Returns the server's response, or None if offline.
async fn ping(
    license_hash: &str,
    license_id: &str,
    email: &str,
    device_hash: &str,
) -> Option<PingResponse> {
    let payload = PingPayload {
        license_hash: license_hash.to_string(),
        license_id: license_id.to_string(),
        email: email.to_string(),
        device_hash: device_hash.to_string(),
        timestamp: Utc::now(),
        version: env!("CARGO_PKG_VERSION").to_string(),
    };

    let client = reqwest::Client::builder()
        .timeout(std::time::Duration::from_secs(5))
        .build()
        .ok()?;

    let resp = client
        .post(TELEMETRY_URL)
        .json(&payload)
        .send()
        .await
        .ok()?;
    if !resp.status().is_success() {
        return None;
    }
    resp.json::<PingResponse>().await.ok()
}

/// Spawn a background telemetry ping. Updates state and downgrades if device limit exceeded.
pub fn spawn_ping(license_hash: String, license_id: String, email: String, device_hash: String) {
    std::thread::spawn(move || {
        let rt = tokio::runtime::Builder::new_current_thread()
            .enable_all()
            .build();

        if let Ok(rt) = rt {
            let response = rt.block_on(ping(&license_hash, &license_id, &email, &device_hash));

            match response {
                Some(resp) if resp.ok => {
                    let mut state = LocalState::load_or_create();
                    state.last_ping = Some(Utc::now());
                    state.ever_connected = true;
                    let _ = state.save();

                    if resp.licensed == Some(false) {
                        crate::guard::downgrade_to_community();
                    }
                }
                _ => {}
            }
        }
    });
}
