use std::collections::HashMap;
use std::sync::Arc;

use chrono::{DateTime, Local};
use serde::{Deserialize, Serialize};
use tokio::sync::RwLock;

use crate::config::{ErrorPolicy, JobConfig};

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum JobState {
	Idle,
	Running,
	Paused,
	Failing,
	Stopped,
}

impl std::fmt::Display for JobState {
	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
		match self {
			JobState::Idle => write!(f, "idle"),
			JobState::Running => write!(f, "running"),
			JobState::Paused => write!(f, "paused"),
			JobState::Failing => write!(f, "failing"),
			JobState::Stopped => write!(f, "stopped"),
		}
	}
}

#[derive(Debug, Clone)]
pub struct JobRuntime {
	pub config: JobConfig,
	pub state: JobState,
	pub last_run: Option<DateTime<Local>>,
	pub last_exit: Option<i32>,
	pub next_run: Option<DateTime<Local>>,
	pub consecutive_failures: u32,
	pub backoff_until: Option<DateTime<Local>>,
}

impl JobRuntime {
	pub fn new(config: JobConfig) -> Self {
		Self {
			config,
			state: JobState::Idle,
			last_run: None,
			last_exit: None,
			next_run: None,
			consecutive_failures: 0,
			backoff_until: None,
		}
	}

	pub fn record_success(&mut self) {
		self.last_exit = Some(0);
		self.last_run = Some(Local::now());
		self.consecutive_failures = 0;
		self.backoff_until = None;
		self.state = JobState::Idle;
	}

	pub fn record_failure(&mut self, exit_code: i32) {
		self.last_exit = Some(exit_code);
		self.last_run = Some(Local::now());
		self.consecutive_failures += 1;

		match self.config.on_error {
			ErrorPolicy::Continue => {
				self.state = JobState::Idle;
			}
			ErrorPolicy::Backoff => {
				let delay_secs = 2u64.pow(self.consecutive_failures.min(10));
				let delay = chrono::Duration::seconds(delay_secs as i64);
				self.backoff_until = Some(Local::now() + delay);
				self.state = JobState::Failing;
			}
			ErrorPolicy::Stop => {
				if self.consecutive_failures >= 3 {
					self.state = JobState::Stopped;
					tracing::warn!(
						job = %self.config.name,
						failures = self.consecutive_failures,
						"job stopped after repeated failures"
					);
				} else {
					self.state = JobState::Failing;
				}
			}
		}
	}
}

pub type SchedulerState = Arc<RwLock<HashMap<String, JobRuntime>>>;

pub fn new_state() -> SchedulerState {
	Arc::new(RwLock::new(HashMap::new()))
}

#[cfg(test)]
mod tests {
	use super::*;

	fn make_runtime(policy: ErrorPolicy) -> JobRuntime {
		JobRuntime::new(JobConfig {
			name: "test".to_string(),
			cron: "* * * * *".to_string(),
			command: "echo hi".to_string(),
			dir: None,
			on_error: policy,
			overlap: false,
			timeout: None,
		})
	}

	#[test]
	fn new_runtime_starts_idle() {
		let rt = make_runtime(ErrorPolicy::Continue);
		assert_eq!(rt.state, JobState::Idle);
		assert_eq!(rt.consecutive_failures, 0);
		assert!(rt.last_run.is_none());
		assert!(rt.last_exit.is_none());
		assert!(rt.backoff_until.is_none());
	}

	#[test]
	fn record_success_resets_state() {
		let mut rt = make_runtime(ErrorPolicy::Backoff);
		rt.state = JobState::Failing;
		rt.consecutive_failures = 5;
		rt.backoff_until = Some(Local::now());

		rt.record_success();

		assert_eq!(rt.state, JobState::Idle);
		assert_eq!(rt.consecutive_failures, 0);
		assert!(rt.backoff_until.is_none());
		assert_eq!(rt.last_exit, Some(0));
		assert!(rt.last_run.is_some());
	}

	// -- continue policy --

	#[test]
	fn continue_stays_idle_on_failure() {
		let mut rt = make_runtime(ErrorPolicy::Continue);
		rt.record_failure(1);
		assert_eq!(rt.state, JobState::Idle);
		assert_eq!(rt.consecutive_failures, 1);
		assert_eq!(rt.last_exit, Some(1));
	}

	#[test]
	fn continue_stays_idle_after_many_failures() {
		let mut rt = make_runtime(ErrorPolicy::Continue);
		for _ in 0..10 {
			rt.record_failure(1);
		}
		assert_eq!(rt.state, JobState::Idle);
		assert_eq!(rt.consecutive_failures, 10);
	}

	// -- backoff policy --

	#[test]
	fn backoff_sets_failing_state() {
		let mut rt = make_runtime(ErrorPolicy::Backoff);
		rt.record_failure(1);
		assert_eq!(rt.state, JobState::Failing);
		assert!(rt.backoff_until.is_some());
	}

	#[test]
	fn backoff_exponential_delay() {
		let mut rt = make_runtime(ErrorPolicy::Backoff);
		let before = Local::now();

		rt.record_failure(1); // 1st failure: 2^1 = 2s
		let backoff1 = rt.backoff_until.unwrap();
		let delay1 = (backoff1 - before).num_seconds();
		assert!(delay1 >= 1 && delay1 <= 3, "1st backoff: {}s", delay1);

		rt.record_failure(1); // 2nd failure: 2^2 = 4s
		let backoff2 = rt.backoff_until.unwrap();
		assert!(backoff2 > backoff1);

		rt.record_failure(1); // 3rd failure: 2^3 = 8s
		let backoff3 = rt.backoff_until.unwrap();
		assert!(backoff3 > backoff2);
	}

	#[test]
	fn backoff_caps_at_1024s() {
		let mut rt = make_runtime(ErrorPolicy::Backoff);
		for _ in 0..20 {
			rt.record_failure(1);
		}
		// consecutive_failures = 20, but min(20, 10) = 10, so 2^10 = 1024s
		let before = Local::now();
		rt.record_failure(1);
		let delay = (rt.backoff_until.unwrap() - before).num_seconds();
		assert!(delay <= 1025, "backoff capped: {}s", delay);
	}

	#[test]
	fn backoff_resets_on_success() {
		let mut rt = make_runtime(ErrorPolicy::Backoff);
		rt.record_failure(1);
		rt.record_failure(1);
		assert_eq!(rt.state, JobState::Failing);

		rt.record_success();
		assert_eq!(rt.state, JobState::Idle);
		assert_eq!(rt.consecutive_failures, 0);
		assert!(rt.backoff_until.is_none());
	}

	// -- stop policy --

	#[test]
	fn stop_failing_before_threshold() {
		let mut rt = make_runtime(ErrorPolicy::Stop);
		rt.record_failure(1);
		assert_eq!(rt.state, JobState::Failing);
		assert_eq!(rt.consecutive_failures, 1);

		rt.record_failure(1);
		assert_eq!(rt.state, JobState::Failing);
		assert_eq!(rt.consecutive_failures, 2);
	}

	#[test]
	fn stop_stops_at_three_failures() {
		let mut rt = make_runtime(ErrorPolicy::Stop);
		rt.record_failure(1);
		rt.record_failure(1);
		rt.record_failure(1);
		assert_eq!(rt.state, JobState::Stopped);
		assert_eq!(rt.consecutive_failures, 3);
	}

	#[test]
	fn stop_success_before_threshold_resets() {
		let mut rt = make_runtime(ErrorPolicy::Stop);
		rt.record_failure(1);
		rt.record_failure(1);
		assert_eq!(rt.state, JobState::Failing);

		rt.record_success();
		assert_eq!(rt.state, JobState::Idle);
		assert_eq!(rt.consecutive_failures, 0);
	}

	#[test]
	fn failure_records_exit_code() {
		let mut rt = make_runtime(ErrorPolicy::Continue);
		rt.record_failure(42);
		assert_eq!(rt.last_exit, Some(42));

		rt.record_failure(-1);
		assert_eq!(rt.last_exit, Some(-1));
	}

	#[test]
	fn job_state_display() {
		assert_eq!(format!("{}", JobState::Idle), "idle");
		assert_eq!(format!("{}", JobState::Running), "running");
		assert_eq!(format!("{}", JobState::Paused), "paused");
		assert_eq!(format!("{}", JobState::Failing), "failing");
		assert_eq!(format!("{}", JobState::Stopped), "stopped");
	}
}
