//! Synor Mining SDK for Rust.
//! Pool connections, block templates, hashrate stats, and GPU management.

use reqwest::Client;
use serde::{Deserialize, Serialize};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, RwLock};
use std::time::Duration;
use thiserror::Error;

const DEFAULT_ENDPOINT: &str = "https://mining.synor.io/v1";
const DEFAULT_TIMEOUT: u64 = 30;
const DEFAULT_RETRIES: u32 = 3;

/// Device type.
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum DeviceType {
    Cpu,
    GpuNvidia,
    GpuAmd,
    Asic,
}

/// Device status.
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum DeviceStatus {
    Idle,
    Mining,
    Error,
    Offline,
}

/// Connection status.
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum ConnectionStatus {
    Disconnected,
    Connecting,
    Connected,
    Reconnecting,
}

/// Time period for stats.
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum TimePeriod {
    Hour,
    Day,
    Week,
    Month,
    All,
}

/// Submit result status.
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum SubmitResultStatus {
    Accepted,
    Rejected,
    Stale,
}

/// Mining SDK configuration.
#[derive(Debug, Clone)]
pub struct MiningConfig {
    pub api_key: String,
    pub endpoint: String,
    pub timeout: Duration,
    pub retries: u32,
    pub debug: bool,
}

impl MiningConfig {
    pub fn new(api_key: impl Into<String>) -> Self {
        Self {
            api_key: api_key.into(),
            endpoint: DEFAULT_ENDPOINT.to_string(),
            timeout: Duration::from_secs(DEFAULT_TIMEOUT),
            retries: DEFAULT_RETRIES,
            debug: false,
        }
    }

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

/// Pool configuration.
#[derive(Debug, Clone, Serialize)]
pub struct PoolConfig {
    pub url: String,
    pub user: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub password: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub algorithm: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub difficulty: Option<f64>,
}

/// Stratum connection.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StratumConnection {
    pub id: String,
    pub pool: String,
    pub status: ConnectionStatus,
    pub algorithm: String,
    pub difficulty: f64,
    pub connected_at: i64,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub last_share_at: Option<i64>,
    pub accepted_shares: i64,
    pub rejected_shares: i64,
    pub stale_shares: i64,
}

/// Template transaction.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TemplateTransaction {
    pub txid: String,
    pub data: String,
    pub fee: String,
    pub weight: i32,
}

/// Block template.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BlockTemplate {
    pub id: String,
    pub previous_block_hash: String,
    pub merkle_root: String,
    pub timestamp: i64,
    pub bits: String,
    pub height: i64,
    pub coinbase_value: String,
    pub transactions: Vec<TemplateTransaction>,
    pub target: String,
    pub algorithm: String,
    pub extra_nonce: String,
}

/// Mined work to submit.
#[derive(Debug, Clone, Serialize)]
pub struct MinedWork {
    pub template_id: String,
    pub nonce: String,
    pub extra_nonce: String,
    pub timestamp: i64,
    pub hash: String,
}

/// Share info.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ShareInfo {
    pub hash: String,
    pub difficulty: f64,
    pub timestamp: i64,
    pub accepted: bool,
}

/// Submit result.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SubmitResult {
    pub status: SubmitResultStatus,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub reason: Option<String>,
    pub share: ShareInfo,
    pub block_found: bool,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub block_hash: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub reward: Option<String>,
}

/// Hashrate statistics.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Hashrate {
    pub current: f64,
    pub average_1h: f64,
    pub average_24h: f64,
    pub peak: f64,
    pub unit: String,
}

/// Share stats.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ShareStats {
    pub accepted: i64,
    pub rejected: i64,
    pub stale: i64,
    pub total: i64,
    pub accept_rate: f64,
}

/// Device temperature.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DeviceTemperature {
    pub current: f64,
    pub max: f64,
    pub throttling: bool,
}

/// Earnings snapshot.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EarningsSnapshot {
    pub today: String,
    pub yesterday: String,
    pub this_week: String,
    pub this_month: String,
    pub total: String,
    pub currency: String,
}

/// Mining stats.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MiningStats {
    pub hashrate: Hashrate,
    pub shares: ShareStats,
    pub uptime: i64,
    pub efficiency: f64,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub power_consumption: Option<f64>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub temperature: Option<DeviceTemperature>,
    pub earnings: EarningsSnapshot,
}

/// Earnings breakdown.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EarningsBreakdown {
    pub date: i64,
    pub amount: String,
    pub blocks: i32,
    pub shares: i64,
    pub hashrate: f64,
}

/// Detailed earnings.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Earnings {
    pub period: TimePeriod,
    pub start_date: i64,
    pub end_date: i64,
    pub amount: String,
    pub blocks: i32,
    pub shares: i64,
    pub average_hashrate: f64,
    pub currency: String,
    pub breakdown: Vec<EarningsBreakdown>,
}

/// Mining device.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MiningDevice {
    pub id: String,
    pub name: String,
    #[serde(rename = "type")]
    pub device_type: DeviceType,
    pub status: DeviceStatus,
    pub hashrate: f64,
    pub temperature: f64,
    pub fan_speed: f64,
    pub power_draw: f64,
    pub memory_used: i64,
    pub memory_total: i64,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub driver: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub firmware: Option<String>,
}

/// Device configuration.
#[derive(Debug, Clone, Serialize)]
pub struct DeviceConfig {
    pub enabled: bool,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub intensity: Option<i32>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub power_limit: Option<i32>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub core_clock_offset: Option<i32>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub memory_clock_offset: Option<i32>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub fan_speed: Option<i32>,
}

/// Worker info.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WorkerInfo {
    pub id: String,
    pub name: String,
    pub status: ConnectionStatus,
    pub hashrate: Hashrate,
    pub shares: ShareStats,
    pub devices: Vec<MiningDevice>,
    pub last_seen: i64,
    pub uptime: i64,
}

/// Pool stats.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PoolStats {
    pub url: String,
    pub workers: i32,
    pub hashrate: f64,
    pub difficulty: f64,
    pub last_block: i64,
    pub blocks_found_24h: i32,
    pub luck: f64,
}

/// Mining algorithm.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MiningAlgorithm {
    pub name: String,
    pub display_name: String,
    pub hash_unit: String,
    pub profitability: String,
    pub difficulty: f64,
    pub block_reward: String,
    pub block_time: i32,
}

/// Work result from getwork.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WorkResult {
    pub work: String,
    pub target: String,
    pub algorithm: String,
}

/// Start mining result.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StartMiningResult {
    pub started: bool,
    pub devices: Vec<String>,
}

/// Mining SDK error.
#[derive(Error, Debug)]
pub enum MiningError {
    #[error("HTTP error: {message}")]
    Http {
        message: String,
        code: Option<String>,
        status_code: u16,
    },
    #[error("Client closed")]
    ClientClosed,
    #[error("No active connection")]
    NoActiveConnection,
    #[error("Request error: {0}")]
    Request(#[from] reqwest::Error),
    #[error("JSON error: {0}")]
    Json(#[from] serde_json::Error),
}

/// Synor Mining SDK client.
pub struct SynorMining {
    config: MiningConfig,
    client: Client,
    active_connection: Arc<RwLock<Option<StratumConnection>>>,
    closed: Arc<AtomicBool>,
}

impl SynorMining {
    /// Create a new Mining client.
    pub fn new(config: MiningConfig) -> Result<Self, MiningError> {
        let client = Client::builder().timeout(config.timeout).build()?;

        Ok(Self {
            config,
            client,
            active_connection: Arc::new(RwLock::new(None)),
            closed: Arc::new(AtomicBool::new(false)),
        })
    }

    // ==================== Pool Connection ====================

    /// Connect to a mining pool.
    pub async fn connect(&self, pool: PoolConfig) -> Result<StratumConnection, MiningError> {
        let conn: StratumConnection = self.post("/pool/connect", &pool).await?;
        *self.active_connection.write().unwrap() = Some(conn.clone());
        Ok(conn)
    }

    /// Disconnect from the current pool.
    pub async fn disconnect(&self) -> Result<(), MiningError> {
        let conn = self.active_connection.read().unwrap().clone();
        if let Some(c) = conn {
            self.post::<(), ()>(&format!("/pool/disconnect/{}", c.id), &())
                .await?;
            *self.active_connection.write().unwrap() = None;
        }
        Ok(())
    }

    /// Get current connection status.
    pub async fn get_connection_status(&self) -> Result<Option<StratumConnection>, MiningError> {
        let conn = self.active_connection.read().unwrap().clone();
        if let Some(c) = conn {
            let status: StratumConnection =
                self.get(&format!("/pool/status/{}", c.id)).await?;
            Ok(Some(status))
        } else {
            Ok(None)
        }
    }

    /// Reconnect to the pool.
    pub async fn reconnect(&self) -> Result<StratumConnection, MiningError> {
        let conn = self.active_connection.read().unwrap().clone();
        if let Some(c) = conn {
            self.post(&format!("/pool/reconnect/{}", c.id), &()).await
        } else {
            Err(MiningError::NoActiveConnection)
        }
    }

    // ==================== Mining Operations ====================

    /// Get the current block template.
    pub async fn get_block_template(&self) -> Result<BlockTemplate, MiningError> {
        self.get("/mining/template").await
    }

    /// Submit mined work.
    pub async fn submit_work(&self, work: MinedWork) -> Result<SubmitResult, MiningError> {
        self.post("/mining/submit", &work).await
    }

    /// Get work from pool.
    pub async fn get_work(&self) -> Result<WorkResult, MiningError> {
        self.get("/mining/getwork").await
    }

    /// Start mining on all enabled devices.
    pub async fn start_mining(
        &self,
        algorithm: Option<&str>,
    ) -> Result<StartMiningResult, MiningError> {
        #[derive(Serialize)]
        struct Request {
            #[serde(skip_serializing_if = "Option::is_none")]
            algorithm: Option<String>,
        }
        self.post(
            "/mining/start",
            &Request {
                algorithm: algorithm.map(String::from),
            },
        )
        .await
    }

    /// Stop mining on all devices.
    pub async fn stop_mining(&self) -> Result<bool, MiningError> {
        #[derive(Deserialize)]
        struct Response {
            stopped: bool,
        }
        let resp: Response = self.post("/mining/stop", &()).await?;
        Ok(resp.stopped)
    }

    // ==================== Stats ====================

    /// Get current hashrate.
    pub async fn get_hashrate(&self) -> Result<Hashrate, MiningError> {
        self.get("/stats/hashrate").await
    }

    /// Get mining stats.
    pub async fn get_stats(&self) -> Result<MiningStats, MiningError> {
        self.get("/stats").await
    }

    /// Get earnings for a time period.
    pub async fn get_earnings(&self, period: Option<TimePeriod>) -> Result<Earnings, MiningError> {
        let path = match period {
            Some(p) => format!("/stats/earnings?period={:?}", p).to_lowercase(),
            None => "/stats/earnings".to_string(),
        };
        self.get(&path).await
    }

    /// Get earnings history.
    pub async fn get_earnings_history(
        &self,
        limit: Option<i32>,
        offset: Option<i32>,
    ) -> Result<Vec<Earnings>, MiningError> {
        let mut params = Vec::new();
        if let Some(l) = limit {
            params.push(format!("limit={}", l));
        }
        if let Some(o) = offset {
            params.push(format!("offset={}", o));
        }
        let path = if params.is_empty() {
            "/stats/earnings/history".to_string()
        } else {
            format!("/stats/earnings/history?{}", params.join("&"))
        };
        #[derive(Deserialize)]
        struct Response {
            earnings: Vec<Earnings>,
        }
        let resp: Response = self.get(&path).await?;
        Ok(resp.earnings)
    }

    /// Get pool stats.
    pub async fn get_pool_stats(&self) -> Result<PoolStats, MiningError> {
        self.get("/pool/stats").await
    }

    // ==================== GPU Management ====================

    /// List all mining devices.
    pub async fn list_devices(&self) -> Result<Vec<MiningDevice>, MiningError> {
        #[derive(Deserialize)]
        struct Response {
            devices: Vec<MiningDevice>,
        }
        let resp: Response = self.get("/devices").await?;
        Ok(resp.devices)
    }

    /// Get device details.
    pub async fn get_device(&self, device_id: &str) -> Result<MiningDevice, MiningError> {
        self.get(&format!("/devices/{}", urlencoding::encode(device_id)))
            .await
    }

    /// Set device configuration.
    pub async fn set_device_config(
        &self,
        device_id: &str,
        config: DeviceConfig,
    ) -> Result<MiningDevice, MiningError> {
        self.post(
            &format!("/devices/{}/config", urlencoding::encode(device_id)),
            &config,
        )
        .await
    }

    /// Enable a device for mining.
    pub async fn enable_device(&self, device_id: &str) -> Result<MiningDevice, MiningError> {
        self.post::<(), MiningDevice>(
            &format!("/devices/{}/enable", urlencoding::encode(device_id)),
            &(),
        )
        .await
    }

    /// Disable a device.
    pub async fn disable_device(&self, device_id: &str) -> Result<MiningDevice, MiningError> {
        self.post::<(), MiningDevice>(
            &format!("/devices/{}/disable", urlencoding::encode(device_id)),
            &(),
        )
        .await
    }

    /// Reset device to default settings.
    pub async fn reset_device(&self, device_id: &str) -> Result<MiningDevice, MiningError> {
        self.post::<(), MiningDevice>(
            &format!("/devices/{}/reset", urlencoding::encode(device_id)),
            &(),
        )
        .await
    }

    // ==================== Workers ====================

    /// List all workers.
    pub async fn list_workers(&self) -> Result<Vec<WorkerInfo>, MiningError> {
        #[derive(Deserialize)]
        struct Response {
            workers: Vec<WorkerInfo>,
        }
        let resp: Response = self.get("/workers").await?;
        Ok(resp.workers)
    }

    /// Get worker details.
    pub async fn get_worker(&self, worker_id: &str) -> Result<WorkerInfo, MiningError> {
        self.get(&format!("/workers/{}", urlencoding::encode(worker_id)))
            .await
    }

    /// Create a new worker.
    pub async fn create_worker(&self, name: &str) -> Result<WorkerInfo, MiningError> {
        #[derive(Serialize)]
        struct Request {
            name: String,
        }
        self.post(
            "/workers",
            &Request {
                name: name.to_string(),
            },
        )
        .await
    }

    /// Delete a worker.
    pub async fn delete_worker(&self, worker_id: &str) -> Result<(), MiningError> {
        self.delete(&format!("/workers/{}", urlencoding::encode(worker_id)))
            .await
    }

    // ==================== Algorithms ====================

    /// Get supported mining algorithms.
    pub async fn get_supported_algorithms(&self) -> Result<Vec<MiningAlgorithm>, MiningError> {
        #[derive(Deserialize)]
        struct Response {
            algorithms: Vec<MiningAlgorithm>,
        }
        let resp: Response = self.get("/algorithms").await?;
        Ok(resp.algorithms)
    }

    /// Get current algorithm.
    pub async fn get_current_algorithm(&self) -> Result<MiningAlgorithm, MiningError> {
        self.get("/algorithms/current").await
    }

    /// Switch to a different algorithm.
    pub async fn switch_algorithm(&self, algorithm: &str) -> Result<bool, MiningError> {
        #[derive(Serialize)]
        struct Request {
            algorithm: String,
        }
        #[derive(Deserialize)]
        struct Response {
            switched: bool,
        }
        let resp: Response = self
            .post(
                "/algorithms/switch",
                &Request {
                    algorithm: algorithm.to_string(),
                },
            )
            .await?;
        Ok(resp.switched)
    }

    // ==================== Lifecycle ====================

    /// Health check.
    pub async fn health_check(&self) -> bool {
        #[derive(Deserialize)]
        struct Response {
            status: String,
        }
        match self.get::<Response>("/health").await {
            Ok(resp) => resp.status == "healthy",
            Err(_) => false,
        }
    }

    /// Check if client is closed.
    pub fn is_closed(&self) -> bool {
        self.closed.load(Ordering::SeqCst)
    }

    /// Close the client.
    pub async fn close(&self) -> Result<(), MiningError> {
        self.disconnect().await?;
        self.closed.store(true, Ordering::SeqCst);
        Ok(())
    }

    // ==================== Private Methods ====================

    async fn get<T: for<'de> Deserialize<'de>>(&self, path: &str) -> Result<T, MiningError> {
        self.execute(|| async {
            let resp = self
                .client
                .get(format!("{}{}", self.config.endpoint, path))
                .header("Authorization", format!("Bearer {}", self.config.api_key))
                .header("Content-Type", "application/json")
                .header("X-SDK-Version", "rust/0.1.0")
                .send()
                .await?;
            self.handle_response(resp).await
        })
        .await
    }

    async fn post<B: Serialize, T: for<'de> Deserialize<'de>>(
        &self,
        path: &str,
        body: &B,
    ) -> Result<T, MiningError> {
        self.execute(|| async {
            let resp = self
                .client
                .post(format!("{}{}", self.config.endpoint, path))
                .header("Authorization", format!("Bearer {}", self.config.api_key))
                .header("Content-Type", "application/json")
                .header("X-SDK-Version", "rust/0.1.0")
                .json(body)
                .send()
                .await?;
            self.handle_response(resp).await
        })
        .await
    }

    async fn delete(&self, path: &str) -> Result<(), MiningError> {
        self.execute(|| async {
            let resp = self
                .client
                .delete(format!("{}{}", self.config.endpoint, path))
                .header("Authorization", format!("Bearer {}", self.config.api_key))
                .header("X-SDK-Version", "rust/0.1.0")
                .send()
                .await?;
            if resp.status().is_success() {
                Ok(())
            } else {
                Err(self.parse_error(resp).await)
            }
        })
        .await
    }

    async fn execute<F, Fut, T>(&self, operation: F) -> Result<T, MiningError>
    where
        F: Fn() -> Fut,
        Fut: std::future::Future<Output = Result<T, MiningError>>,
    {
        if self.closed.load(Ordering::SeqCst) {
            return Err(MiningError::ClientClosed);
        }

        let mut last_error = None;
        for attempt in 0..self.config.retries {
            match operation().await {
                Ok(result) => return Ok(result),
                Err(e) => {
                    if self.config.debug {
                        eprintln!("Attempt {} failed: {:?}", attempt + 1, e);
                    }
                    last_error = Some(e);
                    if attempt < self.config.retries - 1 {
                        tokio::time::sleep(Duration::from_secs(1 << attempt)).await;
                    }
                }
            }
        }
        Err(last_error.unwrap())
    }

    async fn handle_response<T: for<'de> Deserialize<'de>>(
        &self,
        resp: reqwest::Response,
    ) -> Result<T, MiningError> {
        if resp.status().is_success() {
            Ok(resp.json().await?)
        } else {
            Err(self.parse_error(resp).await)
        }
    }

    async fn parse_error(&self, resp: reqwest::Response) -> MiningError {
        let status = resp.status().as_u16();
        let body = resp.text().await.unwrap_or_default();

        #[derive(Deserialize)]
        struct ErrorResponse {
            message: Option<String>,
            code: Option<String>,
        }

        let (message, code) = match serde_json::from_str::<ErrorResponse>(&body) {
            Ok(err) => (
                err.message.unwrap_or_else(|| format!("HTTP {}", status)),
                err.code,
            ),
            Err(_) => (format!("HTTP {}", status), None),
        };

        MiningError::Http {
            message,
            code,
            status_code: status,
        }
    }
}
