use std::fs::{self, OpenOptions};
use std::io::Write;
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex};

use anyhow::Context;
use chrono::{Datelike, Timelike};
use serde_json::{json, Value};

use crate::json_util::now_iso;

const DEFAULT_LOG_RETENTION_BYTES: u64 = 1024 * 1024 * 1024;

#[derive(Clone)]
pub struct Logger {
    service: String,
    root_dir: PathBuf,
    prefix: String,
    suffix: String,
    max_bytes: u64,
    recent: Arc<Mutex<Vec<Value>>>,
    last_prune_ms: Arc<Mutex<i64>>,
}

impl Logger {
    pub fn new(service: &str, log_file: &str) -> anyhow::Result<Self> {
        let service = safe_service_name(service);
        let (root_dir, prefix, suffix) = parse_base(log_file, &service);
        let max_bytes = log_retention_bytes_for_service(&service);
        let logger = Self {
            service,
            root_dir,
            prefix,
            suffix,
            max_bytes,
            recent: Arc::new(Mutex::new(Vec::new())),
            last_prune_ms: Arc::new(Mutex::new(0)),
        };
        logger.prune()?;
        Ok(logger)
    }

    pub fn log(&self, level: &str, message: &str, data: Option<Value>) {
        let entry = match data {
            Some(data) => {
                json!({ "ts": now_iso(), "service": self.service, "level": level, "message": message, "data": data })
            }
            None => {
                json!({ "ts": now_iso(), "service": self.service, "level": level, "message": message })
            }
        };
        {
            let mut recent = self.recent.lock().expect("recent logs poisoned");
            recent.push(entry.clone());
            while recent.len() > 500 {
                recent.remove(0);
            }
        }
        let line = format!("{entry}\n");
        if let Err(error) = self.append_line(&line) {
            eprintln!(
                "{}",
                json!({ "ts": now_iso(), "service": self.service, "level": "error", "message": "log_write_failed", "data": error.to_string() })
            );
        }
        match level {
            "error" => eprintln!("{}", line.trim_end()),
            "warn" => eprintln!("{}", line.trim_end()),
            _ => println!("{}", line.trim_end()),
        }
    }

    pub fn recent(&self, limit: usize) -> Vec<Value> {
        let recent = self.recent.lock().expect("recent logs poisoned");
        recent
            .iter()
            .rev()
            .take(limit)
            .cloned()
            .collect::<Vec<_>>()
            .into_iter()
            .rev()
            .collect()
    }

    fn current_path(&self) -> PathBuf {
        let now = chrono::Local::now();
        let day = format!("{:04}{:02}{:02}", now.year(), now.month(), now.day());
        let hour = format!("{:02}", now.hour());
        self.root_dir
            .join(day.clone())
            .join(format!("{}_{}_{}{}", self.prefix, day, hour, self.suffix))
    }

    fn append_line(&self, line: &str) -> anyhow::Result<()> {
        let path = self.current_path();
        if let Some(parent) = path.parent() {
            fs::create_dir_all(parent)
                .with_context(|| format!("create log directory {}", parent.display()))?;
        }
        let mut file = OpenOptions::new()
            .create(true)
            .append(true)
            .open(&path)
            .with_context(|| format!("open log file {}", path.display()))?;
        file.write_all(line.as_bytes())?;
        self.maybe_prune();
        Ok(())
    }

    fn maybe_prune(&self) {
        let now = chrono::Utc::now().timestamp_millis();
        let should_prune = {
            let mut last = self.last_prune_ms.lock().expect("last prune poisoned");
            if now - *last < 60_000 {
                false
            } else {
                *last = now;
                true
            }
        };
        if should_prune {
            let _ = self.prune();
        }
    }

    fn prune(&self) -> anyhow::Result<()> {
        let active = self.current_path();
        let mut files = collect_files(&self.root_dir, &self.suffix)?;
        files.sort_by(|left, right| {
            if left.path == active {
                return std::cmp::Ordering::Greater;
            }
            if right.path == active {
                return std::cmp::Ordering::Less;
            }
            left.path
                .cmp(&right.path)
                .then(left.modified.cmp(&right.modified))
        });
        let mut total: u64 = files.iter().map(|file| file.size).sum();
        for file in files {
            if total <= self.max_bytes {
                break;
            }
            if file.path == active {
                continue;
            }
            if fs::remove_file(&file.path).is_ok() {
                total = total.saturating_sub(file.size);
            }
        }
        Ok(())
    }
}

struct FileEntry {
    path: PathBuf,
    size: u64,
    modified: std::time::SystemTime,
}

fn safe_service_name(service: &str) -> String {
    let filtered = service
        .chars()
        .map(|ch| {
            if ch.is_ascii_alphanumeric() || ch == '_' || ch == '.' || ch == '-' {
                ch
            } else {
                '-'
            }
        })
        .collect::<String>()
        .trim_matches('-')
        .to_string();
    if filtered.is_empty() {
        "service".to_string()
    } else {
        filtered
    }
}

fn parse_base(base_log_file: &str, service: &str) -> (PathBuf, String, String) {
    let suffix = format!("_{service}.jsonl");
    let path = Path::new(base_log_file);
    let dir = path.parent().unwrap_or_else(|| Path::new("."));
    let dir_name = dir.file_name().and_then(|name| name.to_str()).unwrap_or("");
    let root_dir = if dir_name.len() == 8 && dir_name.chars().all(|ch| ch.is_ascii_digit()) {
        dir.parent().unwrap_or(dir).to_path_buf()
    } else {
        dir.to_path_buf()
    };
    let file_name = path
        .file_name()
        .and_then(|name| name.to_str())
        .unwrap_or("");
    let stem = file_name.strip_suffix(".jsonl").unwrap_or(file_name);
    let prefix = file_name
        .strip_suffix(&suffix)
        .map(ToOwned::to_owned)
        .or_else(|| stem.rsplit_once('_').map(|(left, _)| left.to_string()))
        .unwrap_or_else(|| "unidesk".to_string());
    (
        root_dir,
        if prefix.is_empty() {
            "unidesk".to_string()
        } else {
            prefix
        },
        suffix,
    )
}

fn parse_retention(raw: &str, fallback: u64) -> u64 {
    let trimmed = raw.trim();
    if trimmed.is_empty() {
        return fallback;
    }
    let split = trimmed
        .find(|ch: char| !(ch.is_ascii_digit() || ch == '.'))
        .unwrap_or(trimmed.len());
    let number = trimmed[..split].parse::<f64>().unwrap_or(fallback as f64);
    let unit = trimmed[split..].trim().to_ascii_lowercase();
    let multiplier = match unit.as_str() {
        "g" | "gb" | "gib" => 1024.0 * 1024.0 * 1024.0,
        "m" | "mb" | "mib" => 1024.0 * 1024.0,
        "k" | "kb" | "kib" => 1024.0,
        _ => 1.0,
    };
    let bytes = number * multiplier;
    if bytes.is_finite() && bytes > 0.0 {
        bytes.floor() as u64
    } else {
        fallback
    }
}

fn log_retention_bytes_for_service(service: &str) -> u64 {
    let global = std::env::var("UNIDESK_LOG_RETENTION_BYTES")
        .map(|value| parse_retention(&value, DEFAULT_LOG_RETENTION_BYTES))
        .unwrap_or(DEFAULT_LOG_RETENTION_BYTES);
    let env_name = format!(
        "UNIDESK_{}_LOG_MAX_BYTES",
        service
            .to_ascii_uppercase()
            .replace(|ch: char| !ch.is_ascii_alphanumeric(), "_")
    );
    std::env::var(&env_name)
        .map(|value| parse_retention(&value, global))
        .unwrap_or(global)
}

fn collect_files(root: &Path, suffix: &str) -> anyhow::Result<Vec<FileEntry>> {
    let mut result = Vec::new();
    if !root.exists() {
        return Ok(result);
    }
    fn scan(dir: &Path, suffix: &str, result: &mut Vec<FileEntry>) -> anyhow::Result<()> {
        for entry in fs::read_dir(dir)? {
            let entry = entry?;
            let path = entry.path();
            let metadata = match entry.metadata() {
                Ok(metadata) => metadata,
                Err(_) => continue,
            };
            if metadata.is_dir() {
                let _ = scan(&path, suffix, result);
            } else if metadata.is_file()
                && path
                    .file_name()
                    .and_then(|name| name.to_str())
                    .is_some_and(|name| name.ends_with(suffix))
            {
                result.push(FileEntry {
                    path,
                    size: metadata.len(),
                    modified: metadata
                        .modified()
                        .unwrap_or(std::time::SystemTime::UNIX_EPOCH),
                });
            }
        }
        Ok(())
    }
    scan(root, suffix, &mut result)?;
    Ok(result)
}
