use std::sync::Arc;

use gotcha_gitea::Client;

use crate::*;

#[uniffi::export(async_runtime = "tokio")]
impl GotchaCore {
    #[uniffi::constructor]
    pub fn new(storage_directory: Option<String>) -> Arc<Self> {
        let (preferences, startup_error) = match load_preferences(storage_directory.as_deref()) {
            Ok(preferences) => (preferences, None),
            Err(error) => (Preferences::default(), Some(error)),
        };
        let active_server = preferences
            .last_server
            .filter(|index| *index < preferences.servers.len())
            .or_else(|| (preferences.servers.len() == 1).then_some(0));
        Arc::new(Self {
            state: Mutex::new(State {
                preferences,
                active_server,
                ..Default::default()
            }),
            server_activity: tokio::sync::Mutex::new(None),
            startup_error,
        })
    }

    pub fn startup_error(&self) -> Option<String> {
        self.startup_error.clone()
    }

    pub fn servers(&self) -> Vec<ServerRow> {
        self.state
            .lock()
            .unwrap()
            .preferences
            .servers
            .iter()
            .map(|server| ServerRow {
                id: server.credential_account.clone(),
                name: server.name.clone(),
                url: server.url.clone(),
            })
            .collect()
    }

    pub fn active_server_index(&self) -> Option<u32> {
        self.state
            .lock()
            .unwrap()
            .active_server
            .map(|index| index as u32)
    }

    pub fn active_server_name(&self) -> Option<String> {
        let state = self.state.lock().unwrap();
        state
            .active_server
            .and_then(|index| state.preferences.servers.get(index))
            .map(|server| server.name.clone())
    }

    pub fn server_editor(&self, index: u32) -> Result<ServerEditor, GotchaError> {
        let state = self.state.lock().unwrap();
        let server = state
            .preferences
            .servers
            .get(index as usize)
            .ok_or("That server no longer exists.")?;
        Ok(ServerEditor {
            name: server.name.clone(),
            url: server.url.clone(),
            provider: server.provider.into(),
        })
    }

    pub fn select_server(&self, index: u32) -> Result<(), GotchaError> {
        let mut state = self.state.lock().unwrap();
        let index = index as usize;
        if index >= state.preferences.servers.len() {
            return Err("That server no longer exists.".into());
        }
        state.active_server = Some(index);
        state.preferences.last_server = Some(index);
        state.repositories.clear();
        save_preferences(&state.preferences).map_err(Into::into)
    }

    pub async fn add_server(
        &self,
        name: String,
        url: String,
        token: String,
        provider: ServerProvider,
    ) -> Result<u32, GotchaError> {
        let mut server = validate_server(&name, &url, &token, provider.into())?;
        let client = Client::discover(&server.url, Some(&server.token), server.provider)
            .await
            .map_err(|error| error.to_string())?;
        client
            .current_user()
            .await
            .map_err(|error| error.to_string())?;
        server.provider = client.provider();
        let mut state = self.state.lock().unwrap();
        assign_server_credential_account(&mut server, &state.preferences.servers);
        save_server_token(&server)?;
        let mut preferences = state.preferences.clone();
        preferences.servers.push(server.clone());
        let index = preferences.servers.len() - 1;
        preferences.last_server = Some(index);
        if let Err(error) = save_preferences(&preferences) {
            return Err(rollback_added_server_token(&server, error).into());
        }
        state.preferences = preferences;
        state.active_server = Some(index);
        Ok(index as u32)
    }

    pub async fn update_server(
        &self,
        index: u32,
        name: String,
        url: String,
        token: String,
        provider: ServerProvider,
    ) -> Result<(), GotchaError> {
        let index = index as usize;
        let old_server = self
            .state
            .lock()
            .unwrap()
            .preferences
            .servers
            .get(index)
            .cloned()
            .ok_or("That server no longer exists.")?;
        let token = if token.trim().is_empty() {
            old_server.token.clone()
        } else {
            token
        };
        let mut server = validate_server(&name, &url, &token, provider.into())?;
        server.credential_account = old_server.credential_account.clone();
        let client = Client::discover(&server.url, Some(&server.token), server.provider)
            .await
            .map_err(|error| error.to_string())?;
        client
            .current_user()
            .await
            .map_err(|error| error.to_string())?;
        server.provider = client.provider();

        let mut state = self.state.lock().unwrap();
        let current = state
            .preferences
            .servers
            .get(index)
            .ok_or("That server no longer exists.")?;
        if current.credential_account != old_server.credential_account {
            return Err("That server changed while it was being verified.".into());
        }
        save_server_token(&server)?;
        let mut preferences = state.preferences.clone();
        preferences.servers[index] = server;
        if old_server.url != preferences.servers[index].url
            && preferences
                .servers
                .iter()
                .all(|saved| saved.url != old_server.url)
        {
            let new_url = preferences.servers[index].url.clone();
            migrate_server_settings(&mut preferences, &old_server.url, &new_url);
        }
        if let Err(error) = save_preferences(&preferences) {
            return Err(rollback_updated_server_token(&old_server, error).into());
        }
        state.preferences = preferences;
        state.repositories.clear();
        Ok(())
    }

    pub async fn delete_server(&self, index: u32) -> Result<(), GotchaError> {
        let index = index as usize;
        let mut state = self.state.lock().unwrap();
        let server = state
            .preferences
            .servers
            .get(index)
            .cloned()
            .ok_or("That server no longer exists.")?;
        let mut preferences = state.preferences.clone();
        preferences.servers.remove(index);
        preferences
            .notification_cursors
            .remove(&server.credential_account);
        let active_server =
            active_server_after_removal(state.active_server, index, preferences.servers.len());
        preferences.last_server = active_server;
        if preferences
            .servers
            .iter()
            .all(|saved| saved.url != server.url)
        {
            remove_server_settings(&mut preferences, &server.url);
        }
        save_preferences(&preferences)?;
        if let Err(error) = delete_server_token(&server) {
            if let Err(rollback) = save_preferences(&state.preferences) {
                return Err(format!(
                    "{error} Restoring the server configuration also failed: {rollback}"
                )
                .into());
            }
            return Err(error.into());
        }
        state.preferences = preferences;
        state.active_server = active_server;
        state.repositories.clear();
        Ok(())
    }

    pub fn settings(&self) -> Settings {
        let state = self.state.lock().unwrap();
        Settings {
            issue_status: state.preferences.issue_status.clone(),
            pull_status: state.preferences.pull_status.clone(),
            appearance: state.preferences.appearance.index() as u32,
            notifications_enabled: state.preferences.notifications_enabled,
        }
    }

    pub fn set_issue_status(&self, status: String) -> Result<(), GotchaError> {
        if !matches!(status.as_str(), "open" | "closed") {
            return Err("Unsupported issue status.".into());
        }
        let mut state = self.state.lock().unwrap();
        state.preferences.issue_status = status;
        save_preferences(&state.preferences).map_err(Into::into)
    }

    pub fn set_pull_status(&self, status: String) -> Result<(), GotchaError> {
        if !matches!(status.as_str(), "open" | "closed") {
            return Err("Unsupported pull-request status.".into());
        }
        let mut state = self.state.lock().unwrap();
        state.preferences.pull_status = status;
        save_preferences(&state.preferences).map_err(Into::into)
    }

    pub fn set_appearance(&self, index: u32) -> Result<(), GotchaError> {
        let appearance =
            AppearanceMode::from_index(index as i32).ok_or("Unsupported appearance.")?;
        let mut state = self.state.lock().unwrap();
        state.preferences.appearance = appearance;
        save_preferences(&state.preferences).map_err(Into::into)
    }

    pub fn set_notifications_enabled(&self, enabled: bool) -> Result<(), GotchaError> {
        let mut state = self.state.lock().unwrap();
        state.preferences.notifications_enabled = enabled;
        save_preferences(&state.preferences).map_err(Into::into)
    }

    pub async fn home(
        &self,
        page: u32,
        filter: HomeActivityFilter,
    ) -> Result<HomePage, GotchaError> {
        let server = self.server()?;
        let name = server.name.clone();
        Ok(home_page(
            name,
            load_home(&server, valid_page(page)?, filter.into()).await?,
        ))
    }

    pub async fn server_activity(&self, page: u32) -> Result<ServerActivityPage, GotchaError> {
        valid_page(page)?;
        let server = self.server()?;
        let mut session = self.server_activity.lock().await;
        if page == 1
            || session
                .as_ref()
                .is_none_or(|session| session.server_id != server.credential_account)
        {
            *session = Some(ServerActivitySession {
                server_id: server.credential_account.clone(),
                next_page: 1,
                pager: server_activity_pager(&server).await?,
            });
        }
        let session = session.as_mut().unwrap();
        if session.next_page != page {
            return Err("Refresh server activity before loading this page.".into());
        }
        let result = load_server_activities(&mut session.pager).await?;
        if result.has_more {
            session.next_page = session
                .next_page
                .checked_add(1)
                .ok_or("Invalid page number.")?;
        }
        Ok(server_activity_page(result))
    }
}

fn rollback_added_server_token(server: &Server, error: String) -> String {
    match delete_server_token(server) {
        Ok(()) => error,
        Err(rollback) => format!("{error} Removing the unused token also failed: {rollback}"),
    }
}

fn rollback_updated_server_token(server: &Server, error: String) -> String {
    match save_server_token(server) {
        Ok(()) => error,
        Err(rollback) => format!("{error} Restoring the previous token also failed: {rollback}"),
    }
}

fn active_server_after_removal(
    active: Option<usize>,
    removed: usize,
    remaining: usize,
) -> Option<usize> {
    match active {
        _ if remaining == 0 => None,
        Some(active) if active > removed => Some(active - 1),
        Some(active) if active == removed => Some(removed.min(remaining - 1)),
        active => active,
    }
}

fn migrate_server_settings(preferences: &mut Preferences, old_url: &str, new_url: &str) {
    for pane in RepositoryPane::ALL {
        let old_prefix = format!("{}|{old_url}|", pane.key());
        let new_prefix = format!("{}|{new_url}|", pane.key());
        let moved: Vec<_> = preferences
            .favorites
            .iter()
            .filter_map(|favorite| {
                favorite
                    .strip_prefix(&old_prefix)
                    .map(|suffix| (favorite.clone(), format!("{new_prefix}{suffix}")))
            })
            .collect();
        for (old, new) in moved {
            preferences.favorites.remove(&old);
            preferences.favorites.insert(new);
        }
    }

    let old_prefix = format!("{old_url}|");
    let new_prefix = format!("{new_url}|");
    let issue_filters: Vec<_> = preferences
        .issue_filters
        .iter()
        .filter_map(|(key, filter)| {
            key.strip_prefix(&old_prefix)
                .map(|suffix| (key.clone(), format!("{new_prefix}{suffix}"), filter.clone()))
        })
        .collect();
    for (old, new, filter) in issue_filters {
        preferences.issue_filters.remove(&old);
        preferences.issue_filters.entry(new).or_insert(filter);
    }
    if let Some(filter) = preferences.pull_filters.remove(old_url) {
        preferences
            .pull_filters
            .entry(new_url.into())
            .or_insert(filter);
    }
}

fn remove_server_settings(preferences: &mut Preferences, url: &str) {
    preferences.favorites.retain(|favorite| {
        RepositoryPane::ALL
            .iter()
            .all(|pane| !favorite.starts_with(&format!("{}|{url}|", pane.key())))
    });
    let prefix = format!("{url}|");
    preferences
        .issue_filters
        .retain(|key, _| !key.starts_with(&prefix));
    preferences.pull_filters.remove(url);
}

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

    fn server(name: &str, url: &str) -> Server {
        Server {
            name: name.into(),
            url: url.into(),
            provider: gotcha_gitea::Provider::Gitea,
            credential_account: name.into(),
            token: "secret".into(),
        }
    }

    #[test]
    fn adjusts_selection_when_servers_are_removed() {
        assert_eq!(active_server_after_removal(Some(0), 0, 0), None);
        assert_eq!(active_server_after_removal(Some(0), 0, 2), Some(0));
        assert_eq!(active_server_after_removal(Some(2), 1, 2), Some(1));
        assert_eq!(active_server_after_removal(Some(0), 2, 2), Some(0));
        assert_eq!(active_server_after_removal(None, 0, 1), None);
    }

    #[test]
    fn migrates_and_removes_server_scoped_settings() {
        let old = "https://old.example.com";
        let new = "https://new.example.com";
        let mut preferences = Preferences {
            servers: vec![server("Work", old)],
            favorites: [format!("issues|{old}|octo/demo")].into_iter().collect(),
            issue_filters: [(
                format!("{old}|octo/demo"),
                IssueFilter {
                    milestone: "v1".into(),
                    ..Default::default()
                },
            )]
            .into_iter()
            .collect(),
            pull_filters: [(
                old.into(),
                PullFilter {
                    milestone: "v2".into(),
                    ..Default::default()
                },
            )]
            .into_iter()
            .collect(),
            ..Default::default()
        };

        migrate_server_settings(&mut preferences, old, new);
        assert!(
            preferences
                .favorites
                .contains(&format!("issues|{new}|octo/demo"))
        );
        assert_eq!(
            preferences.issue_filters[&format!("{new}|octo/demo")].milestone,
            "v1"
        );
        assert_eq!(preferences.pull_filters[new].milestone, "v2");

        remove_server_settings(&mut preferences, new);
        assert!(preferences.favorites.is_empty());
        assert!(preferences.issue_filters.is_empty());
        assert!(preferences.pull_filters.is_empty());
    }
}
