//! Miner service.

use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::sync::Arc;

use borsh::BorshDeserialize;
use tokio::sync::{broadcast, mpsc, RwLock};
use tracing::{debug, error, info, warn};

use synor_mining::{
    BlockMiner, BlockTemplate as MiningBlockTemplate, BlockTemplateBuilder, CoinbaseBuilder,
    MinerCommand, MinerConfig, MinerEvent, MiningResult, MiningStats as CrateMiningStats,
    TemplateTransaction,
};
use synor_types::{Address, Amount, Block, BlockHeader, BlockId, BlueScore, Hash256, Network, Timestamp, Transaction, TxOutput};
use synor_types::block::BlockBody;
use synor_types::transaction::ScriptPubKey;

use crate::config::NodeConfig;
use crate::services::{ConsensusService, MempoolService};

/// Mining statistics for the node.
#[derive(Clone, Debug, Default)]
pub struct MiningStats {
    /// Total hashes computed.
    pub hashes: u64,
    /// Blocks found.
    pub blocks_found: u64,
    /// Current hashrate (H/s).
    pub hashrate: f64,
    /// Last block found timestamp.
    pub last_block_time: u64,
    /// Mining start time.
    pub start_time: u64,
    /// Is currently mining.
    pub is_mining: bool,
    /// Formatted hashrate string.
    pub formatted_hashrate: String,
}

impl From<CrateMiningStats> for MiningStats {
    fn from(stats: CrateMiningStats) -> Self {
        MiningStats {
            hashes: stats.total_hashes,
            blocks_found: stats.blocks_found,
            hashrate: stats.hashrate,
            last_block_time: stats.last_block_time,
            start_time: 0,
            is_mining: false, // Set by service
            formatted_hashrate: stats.formatted_hashrate(),
        }
    }
}

/// Miner service manages block mining using synor-mining crate.
pub struct MinerService {
    /// Consensus reference.
    consensus: Arc<ConsensusService>,

    /// Mempool reference.
    mempool: Arc<MempoolService>,

    /// Coinbase address.
    coinbase_address: Option<Address>,

    /// Raw coinbase address string for display.
    coinbase_address_str: Option<String>,

    /// Number of threads.
    threads: usize,

    /// Extra data for coinbase.
    extra_data: String,

    /// Network type.
    network: Network,

    /// The underlying block miner.
    miner: Arc<BlockMiner>,

    /// Command sender for the miner.
    cmd_tx: mpsc::Sender<MinerCommand>,

    /// Is mining active.
    is_mining: AtomicBool,

    /// Total hashes counter.
    total_hashes: AtomicU64,

    /// Blocks found counter.
    blocks_found: AtomicU64,

    /// Mining stats (local tracking).
    stats: RwLock<MiningStats>,

    /// Is running.
    running: RwLock<bool>,

    /// Current template ID.
    template_id: AtomicU64,

    /// Shutdown receiver.
    shutdown_rx: RwLock<Option<broadcast::Receiver<()>>>,

    /// Block found channel (hash of found blocks).
    block_found_tx: broadcast::Sender<[u8; 32]>,
}

impl MinerService {
    /// Creates a new miner service.
    pub async fn new(
        consensus: Arc<ConsensusService>,
        mempool: Arc<MempoolService>,
        config: &NodeConfig,
        shutdown_rx: broadcast::Receiver<()>,
    ) -> anyhow::Result<Self> {
        let (block_found_tx, _) = broadcast::channel(100);

        let threads = if config.mining.threads == 0 {
            num_cpus::get()
        } else {
            config.mining.threads
        };

        // Parse coinbase address if provided
        let coinbase_address = config
            .mining
            .coinbase_address
            .as_ref()
            .and_then(|addr_str| addr_str.parse::<Address>().ok());

        // Determine network from config
        let network = match config.network.as_str() {
            "testnet" => Network::Testnet,
            "devnet" => Network::Devnet,
            _ => Network::Mainnet,
        };

        // Create miner config
        let miner_address = coinbase_address.clone().unwrap_or_else(|| {
            // Default placeholder address (won't mine without real address)
            Address::from_ed25519_pubkey(network, &[0; 32])
        });

        let miner_config = MinerConfig::solo(miner_address, threads);
        let miner = Arc::new(BlockMiner::new(miner_config));
        let cmd_tx = miner.command_sender();

        Ok(MinerService {
            consensus,
            mempool,
            coinbase_address,
            coinbase_address_str: config.mining.coinbase_address.clone(),
            threads,
            extra_data: config.mining.extra_data.clone(),
            network,
            miner,
            cmd_tx,
            is_mining: AtomicBool::new(false),
            total_hashes: AtomicU64::new(0),
            blocks_found: AtomicU64::new(0),
            stats: RwLock::new(MiningStats::default()),
            running: RwLock::new(false),
            template_id: AtomicU64::new(0),
            shutdown_rx: RwLock::new(Some(shutdown_rx)),
            block_found_tx,
        })
    }

    /// Starts the miner service.
    pub async fn start(self: &Arc<Self>) -> anyhow::Result<()> {
        if self.coinbase_address.is_none() {
            warn!("Mining enabled but no coinbase address set");
            return Ok(());
        }

        info!(
            threads = self.threads,
            address = ?self.coinbase_address_str,
            "Starting miner"
        );

        *self.running.write().await = true;
        self.is_mining.store(true, Ordering::SeqCst);

        // Update stats
        {
            let mut stats = self.stats.write().await;
            stats.is_mining = true;
            stats.start_time = current_timestamp();
        }

        // Subscribe to miner events and spawn event handler
        let mut event_rx = self.miner.subscribe();
        let service = Arc::clone(self);

        tokio::spawn(async move {
            while let Ok(event) = event_rx.recv().await {
                match event {
                    MinerEvent::BlockFound(result) => {
                        info!(
                            nonce = result.nonce,
                            hashes = result.hashes,
                            solve_time_ms = result.solve_time_ms,
                            "Block found!"
                        );

                        // Update stats
                        service.blocks_found.fetch_add(1, Ordering::SeqCst);
                        {
                            let mut stats = service.stats.write().await;
                            stats.blocks_found += 1;
                            stats.last_block_time = current_timestamp();
                        }

                        // Notify listeners
                        let _ = service.block_found_tx.send(*result.pow_hash.as_bytes());

                        // Build and submit the block
                        if let Err(e) = service.submit_found_block(&result).await {
                            error!("Failed to submit found block: {}", e);
                        }

                        // Get new template and continue mining
                        if service.is_mining.load(Ordering::SeqCst) {
                            if let Err(e) = service.update_template().await {
                                warn!("Failed to get new template after block found: {}", e);
                            }
                        }
                    }
                    MinerEvent::StatsUpdate(crate_stats) => {
                        let mut stats = service.stats.write().await;
                        stats.hashes = crate_stats.total_hashes;
                        stats.hashrate = crate_stats.hashrate;
                        stats.formatted_hashrate = crate_stats.formatted_hashrate();
                    }
                    MinerEvent::Started => {
                        info!("Mining started");
                    }
                    MinerEvent::Stopped => {
                        info!("Mining stopped");
                    }
                    MinerEvent::Paused => {
                        info!("Mining paused");
                    }
                    MinerEvent::Resumed => {
                        info!("Mining resumed");
                    }
                    MinerEvent::Error(err) => {
                        error!("Mining error: {}", err);
                    }
                }
            }
        });

        // Run the miner's async loop in background
        let miner = Arc::clone(&self.miner);
        tokio::spawn(async move {
            miner.run().await;
        });

        // Get initial template and start mining
        self.update_template().await?;

        Ok(())
    }

    /// Updates the mining template.
    async fn update_template(&self) -> anyhow::Result<()> {
        let template = self.build_template().await?;
        let _ = self
            .cmd_tx
            .send(MinerCommand::NewTemplate(Arc::new(template)))
            .await;
        Ok(())
    }

    /// Stops the miner service.
    pub async fn stop(&self) -> anyhow::Result<()> {
        info!("Stopping miner");

        self.is_mining.store(false, Ordering::SeqCst);
        *self.running.write().await = false;

        // Send stop command to miner
        let _ = self.cmd_tx.send(MinerCommand::Stop).await;

        {
            let mut stats = self.stats.write().await;
            stats.is_mining = false;
        }

        Ok(())
    }

    /// Checks if mining.
    pub fn is_mining(&self) -> bool {
        self.is_mining.load(Ordering::SeqCst) && self.miner.is_mining()
    }

    /// Gets mining stats.
    pub async fn stats(&self) -> MiningStats {
        // Merge local stats with miner stats
        let crate_stats = self.miner.stats();
        let mut stats = self.stats.read().await.clone();
        stats.hashes = crate_stats.total_hashes;
        stats.hashrate = crate_stats.hashrate;
        stats.formatted_hashrate = crate_stats.formatted_hashrate();
        stats
    }

    /// Sets coinbase address.
    pub async fn set_coinbase_address(&self, address: String) -> anyhow::Result<()> {
        let parsed: Address = address
            .parse()
            .map_err(|e| anyhow::anyhow!("Invalid address: {}", e))?;

        // Update miner config
        let new_config = MinerConfig::solo(parsed, self.threads);
        let _ = self
            .cmd_tx
            .send(MinerCommand::UpdateConfig(new_config))
            .await;

        info!(address = %address, "Updated coinbase address");
        Ok(())
    }

    /// Builds a block template for mining.
    async fn build_template(&self) -> anyhow::Result<MiningBlockTemplate> {
        let coinbase_address = self
            .coinbase_address
            .clone()
            .ok_or_else(|| anyhow::anyhow!("No coinbase address set"))?;

        // Get transactions from mempool
        let max_mass = 500_000u64; // TODO: From config
        let mempool_txs = self.mempool.select_for_block(max_mass).await;

        // Get current DAG tips
        let tips = self.consensus.tips().await;
        let blue_score = self.consensus.blue_score().await;
        let bits = self.consensus.current_difficulty().await;

        // Build coinbase
        let block_reward = self.get_block_reward().await;
        let fees: u64 = mempool_txs.iter().map(|tx| tx.fee).sum();

        let coinbase = CoinbaseBuilder::new(coinbase_address, blue_score)
            .extra_data(self.extra_data.as_bytes().to_vec())
            .reward(block_reward)
            .fees(fees)
            .build();

        // Build template
        let template_id = self.template_id.fetch_add(1, Ordering::SeqCst);

        let mut builder = BlockTemplateBuilder::new()
            .version(1)
            .timestamp(current_timestamp())
            .bits(bits)
            .blue_score(blue_score)
            .coinbase(coinbase)
            .reward(block_reward);

        // Add parents
        for tip in tips {
            builder = builder.add_parent(Hash256::from_bytes(tip));
        }

        // Add transactions
        for tx in mempool_txs {
            let template_tx = TemplateTransaction {
                txid: Hash256::from_bytes(tx.hash),
                data: tx.data,
                fee: tx.fee,
                mass: tx.mass,
            };
            builder = builder.add_transaction(template_tx);
        }

        let template = builder
            .build(template_id)
            .map_err(|e| anyhow::anyhow!("Failed to build template: {}", e))?;

        debug!(
            template_id = template_id,
            parents = template.parent_hashes.len(),
            transactions = template.transactions.len(),
            reward = template.block_reward,
            fees = template.total_fees,
            "Built mining template"
        );

        Ok(template)
    }

    /// Gets current block template (for RPC).
    pub async fn get_template(&self) -> anyhow::Result<MiningBlockTemplate> {
        self.build_template().await
    }

    /// Gets the block reward for current height using consensus reward calculator.
    async fn get_block_reward(&self) -> u64 {
        // Use consensus reward calculator for consistent chromatic halving
        self.consensus.get_next_reward().await.as_sompi()
    }

    /// Calculates coinbase value (block reward + fees).
    pub async fn calculate_coinbase_value(&self) -> u64 {
        let block_reward = self.get_block_reward().await;
        let mempool_stats = self.mempool.stats().await;
        block_reward + mempool_stats.total_fees
    }

    /// Submits a found block to consensus.
    async fn submit_found_block(&self, result: &MiningResult) -> anyhow::Result<()> {
        info!(
            template_id = result.template_id,
            nonce = result.nonce,
            "Submitting found block"
        );

        // Get the template that was mined
        let template = self
            .miner
            .current_template()
            .ok_or_else(|| anyhow::anyhow!("No current template"))?;

        // Build full block from template and mining result
        let block_bytes = self.build_block_bytes(&template, result)?;

        // Validate and process
        let validation = self.consensus.validate_block(&block_bytes).await;
        match validation {
            crate::services::consensus::BlockValidation::Valid => {
                self.consensus.process_block_bytes(&block_bytes).await?;
                info!("Block submitted successfully");
            }
            crate::services::consensus::BlockValidation::Invalid { reason } => {
                warn!(reason = %reason, "Mined block was invalid");
                return Err(anyhow::anyhow!("Invalid block: {}", reason));
            }
            _ => {
                warn!("Unexpected block validation result");
            }
        }

        Ok(())
    }

    /// Builds block bytes from template and mining result.
    fn build_block_bytes(
        &self,
        template: &MiningBlockTemplate,
        result: &MiningResult,
    ) -> anyhow::Result<Vec<u8>> {
        // Build a proper Block struct and serialize with Borsh
        // IMPORTANT: Build transactions first, then compute merkle root for header

        // Build coinbase transaction with unique extra_data per block
        // Include blue_score, timestamp, and nonce to ensure unique txid
        let coinbase_amount = Amount::from_sompi(template.block_reward + template.total_fees);
        let coinbase_output = TxOutput::new(
            coinbase_amount,
            ScriptPubKey::p2pkh(template.coinbase_data.miner_address.payload()),
        );

        // Build unique extra_data: [blue_score (8 bytes)] + [timestamp (8 bytes)] + [nonce (8 bytes)] + [user extra_data]
        let mut extra_data = Vec::with_capacity(24 + template.coinbase_data.extra_data.len());
        extra_data.extend_from_slice(&template.blue_score.to_le_bytes());
        extra_data.extend_from_slice(&template.timestamp.to_le_bytes());
        extra_data.extend_from_slice(&result.nonce.to_le_bytes());
        extra_data.extend_from_slice(&template.coinbase_data.extra_data);

        let coinbase_tx = Transaction::coinbase(
            vec![coinbase_output],
            extra_data,
        );

        // Start with coinbase transaction
        let mut transactions = vec![coinbase_tx];

        // Deserialize and add other transactions from their raw bytes
        for tx in &template.transactions {
            let transaction = Transaction::try_from_slice(&tx.data)
                .map_err(|e| anyhow::anyhow!("Failed to deserialize transaction: {}", e))?;
            transactions.push(transaction);
        }

        // Build body first so we can compute merkle root
        let body = BlockBody { transactions };
        let computed_merkle_root = body.merkle_root();

        // Convert parent hashes to BlockIds
        let parents: Vec<BlockId> = template
            .parent_hashes
            .iter()
            .map(|h| BlockId::from_bytes(*h.as_bytes()))
            .collect();

        // New blocks advance the DAG, so daa_score/blue_score should be +1 from parent
        let new_daa_score = template.blue_score + 1;

        // Build the header with correctly computed merkle root
        let header = BlockHeader {
            version: template.version,
            parents,
            merkle_root: computed_merkle_root,
            accepted_id_merkle_root: template.accepted_id_merkle_root,
            utxo_commitment: template.utxo_commitment,
            timestamp: Timestamp::from_millis(template.timestamp),
            bits: template.bits,
            nonce: result.nonce,
            daa_score: new_daa_score,
            blue_score: BlueScore::new(new_daa_score),
            blue_work: Hash256::default(), // Will be computed during validation
            pruning_point: BlockId::default(),
        };

        // Build the block
        let block = Block { header, body };

        // Serialize with Borsh
        borsh::to_vec(&block)
            .map_err(|e| anyhow::anyhow!("Failed to serialize block: {}", e))
    }

    /// Submits a mined block (for external submission via RPC).
    pub async fn submit_block(&self, block: Vec<u8>) -> anyhow::Result<()> {
        info!("Submitting externally mined block");

        let validation = self.consensus.validate_block(&block).await;
        match validation {
            crate::services::consensus::BlockValidation::Valid => {
                self.consensus.process_block_bytes(&block).await?;

                // Update stats
                self.blocks_found.fetch_add(1, Ordering::SeqCst);
                {
                    let mut stats = self.stats.write().await;
                    stats.blocks_found += 1;
                    stats.last_block_time = current_timestamp();
                }

                // Get hash from block header for notification
                let hash = if block.len() >= 32 {
                    let mut h = [0u8; 32];
                    h.copy_from_slice(
                        &blake3::hash(&block[..96.min(block.len())]).as_bytes()[..32],
                    );
                    h
                } else {
                    [0u8; 32]
                };

                let _ = self.block_found_tx.send(hash);
                info!("External block submitted successfully");
            }
            crate::services::consensus::BlockValidation::Invalid { reason } => {
                warn!(reason = %reason, "Submitted block was invalid");
                return Err(anyhow::anyhow!("Invalid block: {}", reason));
            }
            _ => {
                warn!("Unexpected block validation result");
            }
        }

        Ok(())
    }

    /// Subscribes to found blocks.
    pub fn subscribe_blocks(&self) -> broadcast::Receiver<[u8; 32]> {
        self.block_found_tx.subscribe()
    }

    /// Gets current hashrate.
    pub fn hashrate(&self) -> f64 {
        self.miner.hashrate()
    }

    /// Gets hash count.
    pub fn hash_count(&self) -> u64 {
        self.miner.hash_count()
    }

    /// Pauses mining.
    pub async fn pause(&self) -> anyhow::Result<()> {
        let _ = self.cmd_tx.send(MinerCommand::Pause).await;
        Ok(())
    }

    /// Resumes mining.
    pub async fn resume(&self) -> anyhow::Result<()> {
        let _ = self.cmd_tx.send(MinerCommand::Resume).await;
        Ok(())
    }
}

fn current_timestamp() -> u64 {
    std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .unwrap()
        .as_millis() as u64
}
