use std::sync::{Arc, Mutex};

use gotcha_gitea::Client;
use thiserror::Error;

mod activity;
mod api;
mod diff;
mod domain;
mod presentation;
mod storage;

use api::*;
use domain::*;
pub use presentation::*;
use storage::*;

uniffi::setup_scaffolding!();

#[derive(Debug, Error, uniffi::Error)]
pub enum GotchaError {
    #[error("{message}")]
    Message { message: String },
}

impl From<String> for GotchaError {
    fn from(message: String) -> Self {
        Self::Message { message }
    }
}

impl From<&str> for GotchaError {
    fn from(message: &str) -> Self {
        message.to_string().into()
    }
}

#[derive(Clone, uniffi::Record)]
pub struct Settings {
    pub issue_status: String,
    pub pull_status: String,
    pub appearance: u32,
}

#[derive(uniffi::Object)]
pub struct GotchaCore {
    state: Mutex<State>,
    startup_error: Option<String>,
}

#[uniffi::export(async_runtime = "tokio")]
impl GotchaCore {
    #[uniffi::constructor]
    pub fn new() -> Arc<Self> {
        let (preferences, startup_error) = match load_preferences() {
            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()
            }),
            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 {
                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 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,
    ) -> Result<u32, GotchaError> {
        let server = validate_server(&name, &url, &token)?;
        Client::new(&server.url, Some(&server.token))
            .map_err(|error| error.to_string())?
            .current_user()
            .await
            .map_err(|error| error.to_string())?;
        save_server_token(&server)?;
        let mut state = self.state.lock().unwrap();
        state.preferences.servers.push(server);
        let index = state.preferences.servers.len() - 1;
        state.preferences.last_server = Some(index);
        state.active_server = Some(index);
        save_preferences(&state.preferences)?;
        Ok(index as u32)
    }

    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,
        }
    }

    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 async fn home(&self) -> Result<HomePage, GotchaError> {
        let server = self.server()?;
        let name = server.name.clone();
        Ok(home_page(name, load_home(&server).await?))
    }

    pub async fn repositories(&self) -> Result<Vec<RepositoryRow>, GotchaError> {
        let server = self.server()?;
        let repositories = load_repositories(&server).await?;
        let mut state = self.state.lock().unwrap();
        state.repositories = repositories;
        Ok(self.repository_rows(&state))
    }

    pub fn toggle_favorite(
        &self,
        owner: String,
        repository: String,
    ) -> Result<Vec<RepositoryRow>, GotchaError> {
        let mut state = self.state.lock().unwrap();
        let server = state
            .active_server
            .and_then(|index| state.preferences.servers.get(index))
            .ok_or("Select a server first.")?;
        let key = favorite_key(&server.url, &owner, &repository);
        if !state.preferences.favorites.remove(&key) {
            state.preferences.favorites.insert(key);
        }
        save_preferences(&state.preferences)?;
        Ok(self.repository_rows(&state))
    }

    pub async fn issues(
        &self,
        owner: String,
        repository: String,
    ) -> Result<Vec<IssueRow>, GotchaError> {
        let server = self.server()?;
        let status = self.state.lock().unwrap().preferences.issue_status.clone();
        Ok(issue_rows(
            &load_issues(&server, &owner, &repository, &status).await?,
        ))
    }

    pub async fn issue(
        &self,
        owner: String,
        repository: String,
        number: i64,
    ) -> Result<IssuePage, GotchaError> {
        Ok(issue_page(
            load_issue(&self.server()?, &owner, &repository, number).await?,
        ))
    }

    pub async fn milestones(
        &self,
        owner: String,
        repository: String,
    ) -> Result<Vec<MilestoneRow>, GotchaError> {
        Ok(milestone_rows(
            &load_milestones(&self.server()?, &owner, &repository).await?,
        ))
    }

    pub async fn milestone(
        &self,
        owner: String,
        repository: String,
        id: i64,
    ) -> Result<MilestonePage, GotchaError> {
        Ok(milestone_page(
            load_milestone(&self.server()?, &owner, &repository, id).await?,
        ))
    }

    pub async fn pulls(&self) -> Result<Vec<PullRow>, GotchaError> {
        let server = self.server()?;
        let status = self.state.lock().unwrap().preferences.pull_status.clone();
        Ok(pull_rows(&load_pulls(&server, &status).await?))
    }

    pub async fn pull(
        &self,
        owner: String,
        repository: String,
        number: i64,
    ) -> Result<PullPage, GotchaError> {
        Ok(pull_page(
            load_pull(&self.server()?, &owner, &repository, number).await?,
        ))
    }

    pub async fn commits(
        &self,
        owner: String,
        repository: String,
        branch: Option<String>,
    ) -> Result<CommitPage, GotchaError> {
        let server = self.server()?;
        let default_branch = self
            .state
            .lock()
            .unwrap()
            .repositories
            .iter()
            .find(|candidate| candidate.owner == owner && candidate.name == repository)
            .map(|candidate| candidate.default_branch.clone())
            .unwrap_or_else(|| "main".into());
        let branches = load_branches(&server, &owner, &repository, &default_branch).await?;
        let commits = match branch.as_deref() {
            Some(branch) => load_branch_commits(&server, &owner, &repository, branch).await?,
            None => load_all_commits(&server, &owner, &repository, &branches).await?,
        };
        Ok(commit_page(branches, &commits, branch.is_none()))
    }

    pub async fn repository_contents(
        &self,
        owner: String,
        repository: String,
        path: String,
    ) -> Result<Vec<RepositoryContentRow>, GotchaError> {
        Ok(repository_content_rows(
            load_repository_contents(&self.server()?, &owner, &repository, &path).await?,
        ))
    }

    pub async fn repository_file(
        &self,
        owner: String,
        repository: String,
        path: String,
    ) -> Result<Vec<u8>, GotchaError> {
        Ok(load_repository_file(&self.server()?, &owner, &repository, &path).await?)
    }

    pub async fn commit_files(
        &self,
        owner: String,
        repository: String,
        sha: String,
    ) -> Result<Vec<FileRow>, GotchaError> {
        Ok(commit_file_rows(
            load_commit_files(&self.server()?, &owner, &repository, &sha).await?,
        ))
    }

    pub async fn commit_diff(
        &self,
        owner: String,
        repository: String,
        sha: String,
        path: String,
    ) -> Result<DiffPage, GotchaError> {
        Ok(diff_page(
            &path,
            load_commit_diff(&self.server()?, &owner, &repository, &sha, &path).await?,
        ))
    }

    pub async fn pull_diff(
        &self,
        owner: String,
        repository: String,
        number: i64,
        path: String,
    ) -> Result<DiffPage, GotchaError> {
        Ok(diff_page(
            &path,
            load_pull_diff(&self.server()?, &owner, &repository, number, &path).await?,
        ))
    }
}

impl GotchaCore {
    fn server(&self) -> Result<Server, GotchaError> {
        let state = self.state.lock().unwrap();
        state
            .active_server
            .and_then(|index| state.preferences.servers.get(index))
            .cloned()
            .ok_or_else(|| "Select a server first.".to_string().into())
    }

    fn repository_rows(&self, state: &State) -> Vec<RepositoryRow> {
        let server_url = state
            .active_server
            .and_then(|index| state.preferences.servers.get(index))
            .map(|server| server.url.as_str())
            .unwrap_or_default();
        repository_rows(&state.repositories, |repository| {
            state.preferences.favorites.contains(&favorite_key(
                server_url,
                &repository.owner,
                &repository.name,
            ))
        })
    }
}
