use std::os::unix::fs::MetadataExt;
use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering};
/// Support for loading site manifests.
use std::time::{Duration, Instant, SystemTime};

use anyhow::{Error, Result};
use log::*;
use smol::unblock;
use std::io;

use super::SiteEngine;
use super::xman::ExecutableManifest;
use crate::manifest::{generate_manifest, load_manifest};

const RESTAT_MS: u64 = 100;
const RESCAN_MS: u64 = 5000;

/// The manifest load state.
pub struct LoadState {
    manifest: Arc<ExecutableManifest>,
    loaded_at: Instant,
    last_check: AtomicU64,
    file: Option<FileState>,
}

#[derive(Clone, Debug)]
struct FileState {
    mtime: SystemTime,
    size: u64,
}

impl SiteEngine {
    pub async fn manifest(&self) -> Result<Arc<ExecutableManifest>> {
        let now = Instant::now();
        let state = self.manifest_state.load();

        let (elapsed, lcrel) = if let Some(ls) = &*state {
            let elapsed = now.duration_since(ls.loaded_at);
            let lcrel = ls.last_check.load(Ordering::Relaxed);

            // de-bounce the restat load time - very fast path
            let ms = elapsed.as_millis() as u64 - lcrel;
            let delay = if ls.file.is_none() {
                RESCAN_MS
            } else {
                RESTAT_MS
            };
            if ms < delay {
                return Ok(ls.manifest.clone());
            }

            (elapsed, lcrel)
        } else {
            // some non-silly default
            (Duration::new(24 * 60 * 60, 0), 0)
        };

        // upgrade to a full pointer and do more expensive check
        let state = state.clone();

        // stat the manifest file (in-process, this is pretty fast)
        let file_meta = match std::fs::metadata(&self.manifest_file) {
            Ok(m) => Some(m),
            Err(e) if e.kind() == io::ErrorKind::NotFound => None,
            Err(e) => return Err(e.into()),
        };

        // check if we have a current loaded manifest
        if let Some(ls) = state {
            let is_current = match (&file_meta, &ls.file) {
                (Some(cur), Some(old)) => cur.size() == old.size && cur.modified()? == old.mtime,
                _ => false,
            };

            if is_current {
                let _ = ls.last_check.compare_exchange(
                    lcrel,
                    elapsed.as_millis() as u64,
                    Ordering::Relaxed,
                    Ordering::Relaxed,
                );
                return Ok(ls.manifest.clone());
            }
        }

        // if we got this far, we need to reload
        let _lock = self.manifest_lock.lock();
        debug!(
            "elapsed time {}ms, reloading",
            elapsed.as_millis() as u64 - lcrel
        );
        let root = self.root.clone();
        let mfile = self.manifest_file.clone();
        let state = unblock(move || {
            let man = if file_meta.is_some() {
                info!("reloading site manifest");
                load_manifest(&mfile)
            } else {
                info!("regenerating site manifest");
                generate_manifest(&root)
            }?;
            let manifest = ExecutableManifest::try_from(man)?;
            Result::<LoadState, Error>::Ok(LoadState {
                manifest: Arc::new(manifest),
                loaded_at: now,
                last_check: AtomicU64::new(0),
                file: if let Some(fm) = &file_meta {
                    Some(FileState {
                        mtime: fm.modified()?,
                        size: fm.size(),
                    })
                } else {
                    None
                },
            })
        })
        .await?;

        let state = Arc::new(state);
        let ret = state.manifest.clone();
        debug!("storing manifest state");
        self.manifest_state.store(Some(state));
        Ok(ret)
    }
}
