use std::collections::VecDeque;

use crate::current_repo_index::{CurrentRepoIndex, CurrentRepoIndexHandle};
use crate::parallel::config::ParallelPhase1Config;
use crate::parallel::repo_scheduler::{
    InFlightRepoTable, RepoCompletion, RepoRequestAction, TransportCompletion,
    TransportRequestAction, TransportStateTables,
};
use crate::parallel::stats::ParallelRunStats;
use crate::parallel::types::{
    RepoDedupKey, RepoIdentity, RepoKey, RepoRequester, RepoSyncResultEnvelope, RepoSyncTask,
    RepoTransportResultEnvelope, RepoTransportTask, TalInputSpec,
};
use crate::policy::SyncPreference;

pub struct GlobalRunCoordinator {
    pub config: ParallelPhase1Config,
    pub tal_inputs: Vec<TalInputSpec>,
    pub current_repo_index: CurrentRepoIndexHandle,
    pub in_flight_repos: InFlightRepoTable,
    pub transport_tables: TransportStateTables,
    pub pending_repo_tasks: VecDeque<RepoSyncTask>,
    pub pending_transport_tasks: VecDeque<RepoTransportTask>,
    pub stats: ParallelRunStats,
}

impl GlobalRunCoordinator {
    pub fn new(config: ParallelPhase1Config, tal_inputs: Vec<TalInputSpec>) -> Self {
        Self {
            config,
            tal_inputs,
            current_repo_index: CurrentRepoIndex::shared(),
            in_flight_repos: InFlightRepoTable::new(),
            transport_tables: TransportStateTables::new(),
            pending_repo_tasks: VecDeque::new(),
            pending_transport_tasks: VecDeque::new(),
            stats: ParallelRunStats::default(),
        }
    }

    pub fn current_repo_index_handle(&self) -> CurrentRepoIndexHandle {
        self.current_repo_index.clone()
    }

    pub fn register_repo_request(
        &mut self,
        repo_key: RepoKey,
        requester: RepoRequester,
        validation_time: time::OffsetDateTime,
        sync_preference: SyncPreference,
        priority: u8,
    ) -> RepoRequestAction {
        let action = self.in_flight_repos.register_request(
            repo_key,
            requester,
            validation_time,
            sync_preference,
            priority,
        );

        match &action {
            RepoRequestAction::Enqueued(task) => {
                self.stats.repo_tasks_total += 1;
                self.pending_repo_tasks.push_back(task.clone());
                self.stats.repo_queue_depth = self.pending_repo_tasks.len();
            }
            RepoRequestAction::Reused(_) | RepoRequestAction::FailedReuse { .. } => {
                self.stats.repo_tasks_reused += 1;
            }
            RepoRequestAction::Waiting => {}
        }

        action
    }

    pub fn pop_next_repo_task(&mut self) -> Option<RepoSyncTask> {
        let next = self.pending_repo_tasks.pop_front();
        self.stats.repo_queue_depth = self.pending_repo_tasks.len();
        next
    }

    pub fn mark_repo_running(
        &mut self,
        repo_key: &RepoKey,
        started_at: time::OffsetDateTime,
    ) -> Result<(), String> {
        self.in_flight_repos.mark_running(repo_key, started_at)?;
        self.stats.repo_tasks_running += 1;
        Ok(())
    }

    pub fn complete_repo_success(
        &mut self,
        result: RepoSyncResultEnvelope,
        finished_at: time::OffsetDateTime,
    ) -> Result<RepoCompletion, String> {
        let repo_key = result.repo_key.clone();
        let completion = self
            .in_flight_repos
            .complete_success(&repo_key, result, finished_at)?;
        self.stats.repo_tasks_running = self.stats.repo_tasks_running.saturating_sub(1);
        Ok(completion)
    }

    pub fn complete_repo_failure(
        &mut self,
        result: RepoSyncResultEnvelope,
        finished_at: time::OffsetDateTime,
    ) -> Result<RepoCompletion, String> {
        let repo_key = result.repo_key.clone();
        let completion = self
            .in_flight_repos
            .complete_failure(&repo_key, result, finished_at)?;
        self.stats.repo_tasks_running = self.stats.repo_tasks_running.saturating_sub(1);
        self.stats.repo_tasks_failed += 1;
        Ok(completion)
    }

    pub fn register_transport_request(
        &mut self,
        identity: RepoIdentity,
        requester: RepoRequester,
        validation_time: time::OffsetDateTime,
        priority: u8,
        rsync_scope_uri: String,
        rsync_failure_scope_uri: Option<String>,
        sync_preference: SyncPreference,
        retry_short_timeout: bool,
    ) -> TransportRequestAction {
        let mut action = self.transport_tables.register_transport_request(
            identity,
            requester,
            validation_time,
            priority,
            rsync_scope_uri,
            rsync_failure_scope_uri,
            sync_preference,
        );
        match &mut action {
            TransportRequestAction::Enqueue(task) => {
                if retry_short_timeout {
                    task.retry_short_timeout = true;
                }
                self.stats.repo_tasks_total += 1;
                self.pending_transport_tasks.push_back(task.clone());
                self.stats.repo_queue_depth = self.pending_transport_tasks.len();
            }
            TransportRequestAction::ReusedSuccess(_)
            | TransportRequestAction::ReusedTerminalFailure(_) => {
                self.stats.repo_tasks_reused += 1;
            }
            TransportRequestAction::Waiting { .. } => {}
        }
        action
    }

    pub fn push_transport_task(&mut self, task: RepoTransportTask) {
        self.stats.repo_tasks_total += 1;
        self.pending_transport_tasks.push_back(task);
        self.stats.repo_queue_depth = self.pending_transport_tasks.len();
    }

    pub fn pop_next_transport_task(&mut self) -> Option<RepoTransportTask> {
        let next = self.pending_transport_tasks.pop_front();
        self.stats.repo_queue_depth = self.pending_transport_tasks.len();
        next
    }

    pub fn mark_transport_running(
        &mut self,
        dedup_key: &crate::parallel::types::RepoDedupKey,
        started_at: time::OffsetDateTime,
    ) -> Result<(), String> {
        self.transport_tables
            .mark_transport_running(dedup_key, started_at)?;
        self.stats.repo_tasks_running += 1;
        Ok(())
    }

    pub fn complete_transport_result(
        &mut self,
        result: RepoTransportResultEnvelope,
        finished_at: time::OffsetDateTime,
    ) -> Result<TransportCompletion, String> {
        let completion = self
            .transport_tables
            .complete_transport_result(result.clone(), finished_at)?;
        self.stats.repo_tasks_running = self.stats.repo_tasks_running.saturating_sub(1);
        if matches!(
            result.result,
            crate::parallel::types::RepoTransportResultKind::Failed { .. }
        ) && result.mode == crate::parallel::types::RepoTransportMode::Rsync
        {
            self.stats.repo_tasks_failed += 1;
        }
        Ok(completion)
    }

    pub fn runtime_record(
        &self,
        identity: &RepoIdentity,
    ) -> Option<&crate::parallel::repo_scheduler::RepoRuntimeRecord> {
        self.transport_tables.runtime_record(identity)
    }

    pub fn finalized_runtime_records_for_transport(
        &self,
        dedup_key: &RepoDedupKey,
    ) -> Vec<crate::parallel::repo_scheduler::RepoRuntimeRecord> {
        self.transport_tables
            .finalized_runtime_records_for_transport(dedup_key)
    }

    pub fn finalized_runtime_records_for_transport_result(
        &self,
        result: &RepoTransportResultEnvelope,
    ) -> Vec<crate::parallel::repo_scheduler::RepoRuntimeRecord> {
        self.transport_tables
            .finalized_runtime_records_for_transport_result(result)
    }

    pub fn reset_run_state(&mut self) {
        self.in_flight_repos.reset_run_state();
        self.transport_tables.reset_run_state();
        self.pending_repo_tasks.clear();
        self.pending_transport_tasks.clear();
        self.stats = ParallelRunStats::default();
        if let Ok(mut index) = self.current_repo_index.lock() {
            index.clear();
        }
    }
}

#[cfg(test)]
mod tests {
    use crate::parallel::config::ParallelPhase1Config;
    use crate::parallel::repo_scheduler::RepoRequestAction;
    use crate::parallel::run_coordinator::GlobalRunCoordinator;
    use crate::parallel::types::{
        RepoIdentity, RepoKey, RepoRequester, RepoSyncResultEnvelope, RepoSyncResultKind,
        RepoSyncResultRef, TalInputSpec,
    };
    use crate::policy::SyncPreference;
    use crate::storage::{RepositoryViewEntry, RepositoryViewState};

    fn requester(tal_id: &str, rir_id: &str, manifest: &str) -> RepoRequester {
        RepoRequester {
            tal_id: tal_id.to_string(),
            rir_id: rir_id.to_string(),
            parent_node_id: None,
            ca_instance_handle_id: format!("{tal_id}:{manifest}"),
            publication_point_rsync_uri: "rsync://example.test/repo/".to_string(),
            manifest_rsync_uri: manifest.to_string(),
        }
    }

    #[test]
    fn coordinator_holds_tal_inputs_and_default_stats() {
        let coordinator = GlobalRunCoordinator::new(
            ParallelPhase1Config::default(),
            vec![TalInputSpec::from_url("https://example.test/arin.tal")],
        );
        assert_eq!(coordinator.tal_inputs.len(), 1);
        assert_eq!(coordinator.tal_inputs[0].tal_id, "arin");
        assert_eq!(coordinator.stats.repo_tasks_total, 0);
        assert!(coordinator.in_flight_repos.is_empty());
    }

    #[test]
    fn coordinator_enqueues_new_repo_task_once() {
        let mut coordinator = GlobalRunCoordinator::new(
            ParallelPhase1Config::default(),
            vec![TalInputSpec::from_url("https://example.test/arin.tal")],
        );
        let key = RepoKey::new("rsync://example.test/repo/", None);
        let action = coordinator.register_repo_request(
            key.clone(),
            requester("arin", "arin", "rsync://example.test/repo/root.mft"),
            time::OffsetDateTime::UNIX_EPOCH,
            SyncPreference::RrdpThenRsync,
            7,
        );
        let task = match action {
            RepoRequestAction::Enqueued(task) => task,
            other => panic!("expected enqueue, got {other:?}"),
        };
        assert_eq!(task.priority, 7);
        assert_eq!(coordinator.stats.repo_tasks_total, 1);
        assert_eq!(coordinator.stats.repo_queue_depth, 1);

        let popped = coordinator.pop_next_repo_task().expect("task queued");
        assert_eq!(popped.repo_key, key);
        assert_eq!(coordinator.stats.repo_queue_depth, 0);
    }

    #[test]
    fn coordinator_reset_run_state_clears_runtime_only_state() {
        let mut coordinator = GlobalRunCoordinator::new(
            ParallelPhase1Config::default(),
            vec![TalInputSpec::from_url("https://example.test/arin.tal")],
        );
        let identity = RepoIdentity::new(
            Some("https://example.test/notify.xml".to_string()),
            "rsync://example.test/repo/",
        );
        let requester = requester("arin", "arin", "rsync://example.test/repo/root.mft");
        let action = coordinator.register_transport_request(
            identity.clone(),
            requester,
            time::OffsetDateTime::UNIX_EPOCH,
            0,
            "rsync://example.test/repo/".to_string(),
            None,
            SyncPreference::RrdpThenRsync,
            false,
        );
        assert!(matches!(
            action,
            crate::parallel::repo_scheduler::TransportRequestAction::Enqueue(_)
        ));
        assert!(coordinator.runtime_record(&identity).is_some());
        assert_eq!(coordinator.pending_transport_tasks.len(), 1);

        {
            let mut index = coordinator.current_repo_index.lock().expect("index lock");
            index
                .apply_repository_view_entries(&[RepositoryViewEntry {
                    rsync_uri: "rsync://example.test/repo/a.roa".to_string(),
                    current_hash: Some("aa".repeat(32)),
                    repository_source: Some("rsync://example.test/repo/".to_string()),
                    object_type: Some("roa".to_string()),
                    state: RepositoryViewState::Present,
                }])
                .expect("apply current object");
            assert_eq!(index.active_uri_count(), 1);
        }

        coordinator.reset_run_state();

        assert!(coordinator.runtime_record(&identity).is_none());
        assert!(coordinator.pending_transport_tasks.is_empty());
        assert!(coordinator.pending_repo_tasks.is_empty());
        assert_eq!(
            coordinator
                .current_repo_index
                .lock()
                .expect("index lock")
                .active_uri_count(),
            0
        );
        assert_eq!(coordinator.stats.repo_tasks_total, 0);
    }

    #[test]
    fn coordinator_merges_waiting_requesters_and_reuses_success() {
        let mut coordinator = GlobalRunCoordinator::new(
            ParallelPhase1Config::default(),
            vec![
                TalInputSpec::from_url("https://example.test/arin.tal"),
                TalInputSpec::from_url("https://example.test/apnic.tal"),
            ],
        );
        let key = RepoKey::new("rsync://shared.example/repo/", None);
        let _ = coordinator.register_repo_request(
            key.clone(),
            requester("arin", "arin", "rsync://shared.example/repo/root.mft"),
            time::OffsetDateTime::UNIX_EPOCH,
            SyncPreference::RrdpThenRsync,
            0,
        );
        let _ = coordinator.pop_next_repo_task();
        coordinator
            .mark_repo_running(&key, time::OffsetDateTime::UNIX_EPOCH)
            .expect("running");

        let wait_action = coordinator.register_repo_request(
            key.clone(),
            requester("apnic", "apnic", "rsync://shared.example/repo/child.mft"),
            time::OffsetDateTime::UNIX_EPOCH,
            SyncPreference::RrdpThenRsync,
            0,
        );
        assert_eq!(wait_action, RepoRequestAction::Waiting);

        let completion = coordinator
            .complete_repo_success(
                RepoSyncResultEnvelope {
                    repo_key: key.clone(),
                    tal_id: "arin".to_string(),
                    rir_id: "arin".to_string(),
                    result: RepoSyncResultKind::Success(RepoSyncResultRef {
                        repo_key: key.clone(),
                        source: "rrdp".to_string(),
                    }),
                    phase: Some("rrdp_ok".to_string()),
                    timing_ms: 12,
                    warnings: Vec::new(),
                },
                time::OffsetDateTime::UNIX_EPOCH,
            )
            .expect("success");
        assert_eq!(completion.released_requesters.len(), 1);
        assert_eq!(coordinator.stats.repo_tasks_running, 0);

        let reuse_action = coordinator.register_repo_request(
            key,
            requester("ripe", "ripe", "rsync://shared.example/repo/again.mft"),
            time::OffsetDateTime::UNIX_EPOCH,
            SyncPreference::RrdpThenRsync,
            0,
        );
        assert!(matches!(reuse_action, RepoRequestAction::Reused(_)));
        assert_eq!(coordinator.stats.repo_tasks_reused, 1);
    }

    #[test]
    fn coordinator_reuses_failures_without_enqueuing_again() {
        let mut coordinator = GlobalRunCoordinator::new(
            ParallelPhase1Config::default(),
            vec![TalInputSpec::from_url("https://example.test/arin.tal")],
        );
        let key = RepoKey::new("rsync://shared.example/repo/", None);
        let _ = coordinator.register_repo_request(
            key.clone(),
            requester("arin", "arin", "rsync://shared.example/repo/root.mft"),
            time::OffsetDateTime::UNIX_EPOCH,
            SyncPreference::RrdpThenRsync,
            0,
        );
        let _ = coordinator.pop_next_repo_task();
        coordinator
            .mark_repo_running(&key, time::OffsetDateTime::UNIX_EPOCH)
            .expect("running");
        coordinator
            .complete_repo_failure(
                RepoSyncResultEnvelope {
                    repo_key: key.clone(),
                    tal_id: "arin".to_string(),
                    rir_id: "arin".to_string(),
                    result: RepoSyncResultKind::Failed {
                        detail: "timeout".to_string(),
                    },
                    phase: Some("rrdp_failed_rsync_failed".to_string()),
                    timing_ms: 12,
                    warnings: Vec::new(),
                },
                time::OffsetDateTime::UNIX_EPOCH,
            )
            .expect("failure");
        assert_eq!(coordinator.stats.repo_tasks_failed, 1);

        let action = coordinator.register_repo_request(
            key,
            requester("ripe", "ripe", "rsync://shared.example/repo/child.mft"),
            time::OffsetDateTime::UNIX_EPOCH,
            SyncPreference::RrdpThenRsync,
            0,
        );
        assert_eq!(
            action,
            RepoRequestAction::FailedReuse {
                detail: "timeout".to_string()
            }
        );
        assert_eq!(coordinator.stats.repo_tasks_total, 1);
        assert_eq!(coordinator.stats.repo_tasks_reused, 1);
    }
}
