//! SDK initialization gate, credit enforcement, failure reporting, and telemetry.
//!
//! `column_lineage` and `track_lineage` require `_init_sdk(api_key, tenant, backend_url, report_failures, telemetry)`.
//! Credits are checked before execution and deducted only on successful lineage.
//! Failures (empty lineage for valid SQL) are detected and reported automatically.
//!
//! Post-result HTTP calls (count_credit, report_failure, telemetry) run on a single
//! background worker thread with a bounded channel (capacity 64). Callers never block.
//! If the channel is full, messages are silently dropped (best-effort).
//!
//! Telemetry events are sent directly to Azure App Insights (fire-and-forget).
//! Disable via `_init_sdk(..., telemetry=false)` or `ALTIMATE_TELEMETRY=0`.

use pyo3::prelude::*;
use std::sync::mpsc::Sender as StdSender;
use std::sync::mpsc::{SyncSender, sync_channel};
use std::sync::{Mutex, OnceLock};

struct SdkConfig {
    api_key: String,
    tenant: String,
    backend_url: String,
    report_failures: bool,
    telemetry: bool,
}

static SDK_CONFIG: Mutex<Option<SdkConfig>> = Mutex::new(None);

// ---------------------------------------------------------------------------
// Azure App Insights — embedded instrumentation key (not a secret, just a
// routing key). Same pattern as vscode-dbt-power-user.
// ---------------------------------------------------------------------------

const APPINSIGHTS_IKEY: &str = "da5d7672-66ee-4c3c-9be9-29468d9cac6b";
const APPINSIGHTS_ENDPOINT: &str = "https://westus2-2.in.applicationinsights.azure.com/v2/track";

// ---------------------------------------------------------------------------
// Background worker — single thread, bounded channel, fire-and-forget.
// ---------------------------------------------------------------------------

enum BgTask {
    CountCredit {
        api_key: String,
        tenant: String,
        backend_url: String,
        dataset_type: String,
        count: usize,
    },
    ReportFailure {
        api_key: String,
        tenant: String,
        backend_url: String,
        sql: String,
        dialect: String,
        duration_ms: u64,
        reason: String,
    },
    Telemetry {
        event_name: String,
        tenant: String,
        properties: serde_json::Map<String, serde_json::Value>,
        measurements: serde_json::Map<String, serde_json::Value>,
    },
    /// Sentinel: worker sends `()` on the reply channel, then caller knows the queue is drained.
    Flush { reply: StdSender<()> },
    /// Terminate the worker loop.
    Shutdown,
}

static BG_SENDER: OnceLock<SyncSender<BgTask>> = OnceLock::new();

/// Lazily create the background worker on first use.
/// Returns a bounded sender (capacity 64). The worker processes tasks
/// sequentially on a single daemon thread named "sdk-bg".
fn bg_sender() -> &'static SyncSender<BgTask> {
    BG_SENDER.get_or_init(|| {
        let (tx, rx) = sync_channel::<BgTask>(64);
        std::thread::Builder::new()
            .name("sdk-bg".into())
            .spawn(move || {
                while let Ok(task) = rx.recv() {
                    match task {
                        BgTask::CountCredit {
                            api_key,
                            tenant,
                            backend_url,
                            dataset_type,
                            count,
                        } => {
                            let url = format!("{}/sdk/count_credits", backend_url);
                            let _ = ureq::post(&url)
                                .set("Authorization", &format!("Bearer {}", api_key))
                                .set("x-tenant", &tenant)
                                .send_json(serde_json::json!({
                                    "dataset_type": dataset_type,
                                    "count": count,
                                }));
                        }
                        BgTask::ReportFailure {
                            api_key,
                            tenant,
                            backend_url,
                            sql,
                            dialect,
                            duration_ms,
                            reason,
                        } => {
                            let url = format!("{}/sdk/failures", backend_url);
                            let _ = ureq::post(&url)
                                .set("Authorization", &format!("Bearer {}", api_key))
                                .set("x-tenant", &tenant)
                                .send_json(serde_json::json!({
                                    "operation": "column_lineage",
                                    "sql": sql,
                                    "dialect": dialect,
                                    "duration_ms": duration_ms,
                                    "reason": reason,
                                    "sdk_version": "0.1.0",
                                }));
                        }
                        BgTask::Telemetry {
                            event_name,
                            tenant,
                            properties,
                            measurements,
                        } => {
                            let now = chrono_now();
                            let mut props = properties;
                            props.insert("tenant".into(), serde_json::Value::String(tenant));
                            props.insert(
                                "sdk_version".into(),
                                serde_json::Value::String("0.1.0".into()),
                            );
                            let envelope = serde_json::json!({
                                "name": "Microsoft.ApplicationInsights.Event",
                                "time": now,
                                "iKey": APPINSIGHTS_IKEY,
                                "data": {
                                    "baseType": "EventData",
                                    "baseData": {
                                        "ver": 2,
                                        "name": event_name,
                                        "properties": props,
                                        "measurements": measurements,
                                    }
                                }
                            });
                            let _ = ureq::post(APPINSIGHTS_ENDPOINT)
                                .set("Content-Type", "application/json")
                                .send_json(envelope);
                        }
                        BgTask::Flush { reply } => {
                            let _ = reply.send(());
                        }
                        BgTask::Shutdown => break,
                    }
                }
            })
            .expect("Failed to spawn SDK background worker");
        tx
    })
}

/// ISO 8601 timestamp without pulling in chrono.
fn chrono_now() -> String {
    use std::time::{SystemTime, UNIX_EPOCH};
    let d = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .unwrap_or_default();
    let secs = d.as_secs();
    // Good enough for telemetry — seconds precision, UTC.
    let (days, rem) = (secs / 86400, secs % 86400);
    let (h, rem) = (rem / 3600, rem % 3600);
    let (m, s) = (rem / 60, rem % 60);
    // Days since 1970-01-01 → year/month/day (simplified leap-year calc).
    let (y, mo, d) = days_to_ymd(days);
    format!("{y:04}-{mo:02}-{d:02}T{h:02}:{m:02}:{s:02}Z")
}

fn days_to_ymd(mut days: u64) -> (u64, u64, u64) {
    let mut y = 1970;
    loop {
        let dy = if y % 4 == 0 && (y % 100 != 0 || y % 400 == 0) {
            366
        } else {
            365
        };
        if days < dy {
            break;
        }
        days -= dy;
        y += 1;
    }
    let leap = y % 4 == 0 && (y % 100 != 0 || y % 400 == 0);
    let mdays = [
        31,
        if leap { 29 } else { 28 },
        31,
        30,
        31,
        30,
        31,
        31,
        30,
        31,
        30,
        31,
    ];
    let mut mo = 0;
    for md in &mdays {
        if days < *md {
            break;
        }
        days -= md;
        mo += 1;
    }
    (y, mo + 1, days + 1)
}

// ---------------------------------------------------------------------------
// Init / reset
// ---------------------------------------------------------------------------

/// Initialize the SDK with credentials. Called by Python `altimate_core.init()`.
#[pyfunction]
#[pyo3(signature = (api_key, tenant, backend_url, report_failures, telemetry=true))]
pub fn _init_sdk(
    api_key: String,
    tenant: String,
    backend_url: String,
    report_failures: bool,
    telemetry: bool,
) -> PyResult<()> {
    let mut config = SDK_CONFIG
        .lock()
        .map_err(|_| pyo3::exceptions::PyRuntimeError::new_err("SDK config lock poisoned"))?;
    *config = Some(SdkConfig {
        api_key,
        tenant,
        backend_url,
        report_failures,
        telemetry,
    });
    Ok(())
}

/// Reset SDK state. Exposed for test teardown only.
#[pyfunction]
pub fn _reset_sdk() -> PyResult<()> {
    let mut config = SDK_CONFIG
        .lock()
        .map_err(|_| pyo3::exceptions::PyRuntimeError::new_err("SDK config lock poisoned"))?;
    *config = None;
    Ok(())
}

/// Best-effort flush: drain the queue, then shut down the worker thread.
/// Called automatically via Python `atexit`. Never blocks or panics.
#[pyfunction]
pub fn _flush_sdk() -> PyResult<()> {
    if let Some(sender) = BG_SENDER.get() {
        let (tx, rx) = std::sync::mpsc::channel();
        if sender.try_send(BgTask::Flush { reply: tx }).is_ok() {
            let _ = rx.recv_timeout(std::time::Duration::from_secs(5));
        }
        let _ = sender.try_send(BgTask::Shutdown);
    }
    Ok(())
}

/// Gate: returns `Err` if `_init_sdk` has not been called.
pub fn require_init() -> PyResult<()> {
    let config = SDK_CONFIG
        .lock()
        .map_err(|_| pyo3::exceptions::PyRuntimeError::new_err("SDK config lock poisoned"))?;
    if config.is_none() {
        return Err(pyo3::exceptions::PyRuntimeError::new_err(
            "SDK not initialized. Call init(api_key, caller={'tenant': ...}) or create ~/.altimate/altimate.json with {\"altimateApiKey\": \"...\", \"altimateInstanceName\": \"...\", \"altimateUrl\": \"...\"}",
        ));
    }
    Ok(())
}

// ---------------------------------------------------------------------------
// Credit check (synchronous — gates the operation)
// ---------------------------------------------------------------------------

/// Check if credits are available for the given dataset type.
pub fn check_credits(dataset_type: &str) -> PyResult<()> {
    let (api_key, tenant, backend_url) = {
        let config = SDK_CONFIG
            .lock()
            .map_err(|_| pyo3::exceptions::PyRuntimeError::new_err("SDK config lock poisoned"))?;
        let c = config
            .as_ref()
            .ok_or_else(|| pyo3::exceptions::PyRuntimeError::new_err("SDK not initialized"))?;
        (c.api_key.clone(), c.tenant.clone(), c.backend_url.clone())
    };

    let url = format!("{}/sdk/available_credits", backend_url);
    let result = ureq::post(&url)
        .set("Authorization", &format!("Bearer {}", api_key))
        .set("x-tenant", &tenant)
        .send_json(serde_json::json!({"dataset_type": dataset_type}));

    match result {
        Ok(_) => Ok(()),
        Err(ureq::Error::Status(402, _)) => Err(pyo3::exceptions::PyRuntimeError::new_err(
            "No credits remaining",
        )),
        Err(ureq::Error::Status(401, _)) | Err(ureq::Error::Status(403, _)) => Err(
            pyo3::exceptions::PyRuntimeError::new_err("Invalid credentials"),
        ),
        Err(e) => Err(pyo3::exceptions::PyRuntimeError::new_err(format!(
            "Failed to verify credits: {}",
            e
        ))),
    }
}

// ---------------------------------------------------------------------------
// Background: credit deduction, failure reporting, and telemetry
// ---------------------------------------------------------------------------

/// Deduct credits for a successful operation. Enqueues to background worker.
/// Never blocks. Silently drops if queue is full.
pub fn count_credit(dataset_type: &str, count: usize) {
    let (api_key, tenant, backend_url) = match SDK_CONFIG.lock() {
        Ok(guard) => match guard.as_ref() {
            Some(c) => (c.api_key.clone(), c.tenant.clone(), c.backend_url.clone()),
            None => return,
        },
        Err(_) => return,
    };

    let _ = bg_sender().try_send(BgTask::CountCredit {
        api_key,
        tenant,
        backend_url,
        dataset_type: dataset_type.to_owned(),
        count,
    });
}

/// Report a lineage failure to the backend. Enqueues to background worker.
/// Never blocks. Silently drops if queue is full.
pub fn report_failure(sql: &str, dialect: &str, duration_ms: u64, reason: &str) {
    let (api_key, tenant, backend_url) = match SDK_CONFIG.lock() {
        Ok(guard) => match guard.as_ref() {
            Some(c) if should_report(c) => {
                (c.api_key.clone(), c.tenant.clone(), c.backend_url.clone())
            }
            _ => return,
        },
        Err(_) => return,
    };

    let _ = bg_sender().try_send(BgTask::ReportFailure {
        api_key,
        tenant,
        backend_url,
        sql: sql.to_owned(),
        dialect: dialect.to_owned(),
        duration_ms,
        reason: reason.to_owned(),
    });
}

/// Emit a telemetry event to Azure App Insights. Enqueues to background worker.
/// Never blocks. Silently drops if queue is full or telemetry is disabled.
pub fn emit_event(
    event_name: &str,
    properties: serde_json::Map<String, serde_json::Value>,
    measurements: serde_json::Map<String, serde_json::Value>,
) {
    let tenant = match SDK_CONFIG.lock() {
        Ok(guard) => match guard.as_ref() {
            Some(c) if should_emit(c) => c.tenant.clone(),
            _ => return,
        },
        Err(_) => return,
    };

    let _ = bg_sender().try_send(BgTask::Telemetry {
        event_name: event_name.to_owned(),
        tenant,
        properties,
        measurements,
    });
}

/// Emit a timing event for an operation. Convenience wrapper around `emit_event`.
pub fn timed_emit(operation: &str, start: std::time::Instant) {
    let duration_ms = start.elapsed().as_millis() as f64;
    let mut props = serde_json::Map::new();
    props.insert("operation".into(), serde_json::json!(operation));
    let mut measurements = serde_json::Map::new();
    measurements.insert("duration_ms".into(), serde_json::json!(duration_ms));
    emit_event(&format!("sdk/{operation}"), props, measurements);
}

/// Emit a timing event with dialect. Convenience wrapper around `emit_event`.
pub fn timed_emit_with_dialect(operation: &str, dialect: &str, start: std::time::Instant) {
    let duration_ms = start.elapsed().as_millis() as f64;
    let mut props = serde_json::Map::new();
    props.insert("operation".into(), serde_json::json!(operation));
    props.insert("dialect".into(), serde_json::json!(dialect));
    let mut measurements = serde_json::Map::new();
    measurements.insert("duration_ms".into(), serde_json::json!(duration_ms));
    emit_event(&format!("sdk/{operation}"), props, measurements);
}

/// Check if failure reporting is enabled (config flag + env var override).
fn should_report(config: &SdkConfig) -> bool {
    if !config.report_failures {
        return false;
    }
    match std::env::var("ALTIMATE_REPORT_FAILURES") {
        Ok(v) => !["0", "false", "no", "off"].contains(&v.to_lowercase().as_str()),
        Err(_) => true,
    }
}

/// Check if telemetry is enabled (config flag + env var override).
fn should_emit(config: &SdkConfig) -> bool {
    if !config.telemetry {
        return false;
    }
    match std::env::var("ALTIMATE_TELEMETRY") {
        Ok(v) => !["0", "false", "no", "off"].contains(&v.to_lowercase().as_str()),
        Err(_) => true,
    }
}
