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

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

mod api;
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,
        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 repositories(&self, page: u32) -> Result<RepositoryListPage, GotchaError> {
        let server = self.server()?;
        let page_number = valid_page(page)?;
        let repositories = load_repositories(&server, page_number).await?;
        let mut state = self.state.lock().unwrap();
        if page_number == 1 {
            state.repositories.clear();
        }
        for repository in repositories.items {
            if let Some(existing) = state.repositories.iter_mut().find(|candidate| {
                candidate.owner == repository.owner && candidate.name == repository.name
            }) {
                *existing = repository;
            } else {
                state.repositories.push(repository);
            }
        }
        Ok(RepositoryListPage {
            rows: self.repository_rows(&state),
            has_more: repositories.has_more,
        })
    }

    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,
        page: u32,
    ) -> Result<IssueListPage, GotchaError> {
        let server = self.server()?;
        let (status, filter) = {
            let state = self.state.lock().unwrap();
            (
                state.preferences.issue_status.clone(),
                state
                    .preferences
                    .issue_filters
                    .get(&repository_key(&server.url, &owner, &repository))
                    .cloned()
                    .unwrap_or_default(),
            )
        };
        let labels: Vec<_> = filter.labels.into_iter().collect();
        let page = load_issues(
            &server,
            &owner,
            &repository,
            &status,
            &labels,
            &filter.milestone,
            valid_page(page)?,
        )
        .await?;
        Ok(IssueListPage {
            rows: issue_rows(&page.items),
            has_more: page.has_more,
        })
    }

    pub async fn issue_filters(
        &self,
        owner: String,
        repository: String,
    ) -> Result<IssueFilterOptions, GotchaError> {
        let server = self.server()?;
        let filter = self
            .state
            .lock()
            .unwrap()
            .preferences
            .issue_filters
            .get(&repository_key(&server.url, &owner, &repository))
            .cloned()
            .unwrap_or_default();
        let (labels, milestones) = tokio::try_join!(
            load_labels(&server, &owner, &repository),
            load_milestones(&server, &owner, &repository)
        )?;
        Ok(issue_filter_options(&labels, &milestones, &filter))
    }

    pub fn issue_filters_active(
        &self,
        owner: String,
        repository: String,
    ) -> Result<bool, GotchaError> {
        let server = self.server()?;
        let state = self.state.lock().unwrap();
        let filter = state
            .preferences
            .issue_filters
            .get(&repository_key(
                &server.url,
                owner.trim(),
                repository.trim(),
            ))
            .cloned()
            .unwrap_or_default();
        Ok(filter.is_active(&state.preferences.issue_status))
    }

    pub fn set_issue_filters(
        &self,
        owner: String,
        repository: String,
        milestone: String,
        labels: Vec<String>,
    ) -> Result<(), GotchaError> {
        let owner = owner.trim();
        let repository = repository.trim();
        if owner.is_empty() || repository.is_empty() {
            return Err("Select a repository first.".into());
        }
        let filter = IssueFilter {
            milestone,
            labels: labels
                .into_iter()
                .filter(|label| !label.is_empty())
                .collect(),
        };
        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 = repository_key(&server.url, owner, repository);
        if filter == IssueFilter::default() {
            state.preferences.issue_filters.remove(&key);
        } else {
            state.preferences.issue_filters.insert(key, filter);
        }
        save_preferences(&state.preferences).map_err(Into::into)
    }

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

    pub async fn save_issue_comment(
        &self,
        owner: String,
        repository: String,
        number: i64,
        comment_id: Option<i64>,
        body: String,
    ) -> Result<(), GotchaError> {
        let (owner, repository) = validate_repository(&owner, &repository)?;
        if number <= 0 || comment_id.is_some_and(|id| id <= 0) {
            return Err("Invalid issue comment selection.".into());
        }
        if body.trim().is_empty() {
            return Err("Enter a comment.".into());
        }
        save_issue_comment(&self.server()?, owner, repository, number, comment_id, body).await?;
        Ok(())
    }

    pub async fn issue_editor(
        &self,
        owner: String,
        repository: String,
        number: Option<i64>,
    ) -> Result<IssueEditorPage, GotchaError> {
        let (owner, repository) = validate_repository(&owner, &repository)?;
        if number.is_some_and(|number| number <= 0) {
            return Err("Invalid issue number.".into());
        }
        Ok(issue_editor_page(
            load_issue_editor(&self.server()?, owner, repository, number).await?,
        ))
    }

    #[allow(clippy::too_many_arguments)]
    pub async fn save_issue(
        &self,
        owner: String,
        repository: String,
        number: Option<i64>,
        title: String,
        body: String,
        label_ids: Vec<i64>,
        milestone_id: Option<i64>,
        due_date: Option<i64>,
    ) -> Result<i64, GotchaError> {
        let (owner, repository) = validate_repository(&owner, &repository)?;
        let title = title.trim();
        if title.is_empty() {
            return Err("Enter an issue title.".into());
        }
        if number.is_some_and(|number| number <= 0)
            || milestone_id.is_some_and(|id| id <= 0)
            || label_ids.iter().any(|id| *id <= 0)
        {
            return Err("Invalid issue editor selection.".into());
        }
        let label_ids: Vec<_> = label_ids
            .into_iter()
            .collect::<std::collections::BTreeSet<_>>()
            .into_iter()
            .collect();
        let draft = IssueDraft {
            title: title.into(),
            body,
            label_ids,
            milestone_id,
            due_date,
        };
        let server = self.server()?;
        let issue = match number {
            Some(number) => edit_issue(&server, owner, repository, number, draft).await?,
            None => create_issue(&server, owner, repository, draft).await?,
        };
        issue
            .number
            .ok_or_else(|| "The saved issue has no number.".into())
    }

    pub async fn milestones(
        &self,
        owner: String,
        repository: String,
        page: u32,
    ) -> Result<MilestoneListPage, GotchaError> {
        let page =
            load_milestones_page(&self.server()?, &owner, &repository, valid_page(page)?).await?;
        Ok(MilestoneListPage {
            rows: milestone_rows(&page.items),
            has_more: page.has_more,
        })
    }

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

    pub async fn milestone_editor(
        &self,
        owner: String,
        repository: String,
        id: Option<i64>,
    ) -> Result<MilestoneEditorPage, GotchaError> {
        let (owner, repository) = validate_repository(&owner, &repository)?;
        if id.is_some_and(|id| id <= 0) {
            return Err("Invalid milestone selection.".into());
        }
        let milestone = match id {
            Some(id) => Some(load_milestone_editor(&self.server()?, owner, repository, id).await?),
            None => None,
        };
        Ok(milestone_editor_page(milestone))
    }

    pub async fn save_milestone(
        &self,
        owner: String,
        repository: String,
        id: Option<i64>,
        title: String,
        description: String,
        due_date: Option<i64>,
    ) -> Result<i64, GotchaError> {
        let (owner, repository) = validate_repository(&owner, &repository)?;
        let title = title.trim();
        if title.is_empty() {
            return Err("Enter a milestone title.".into());
        }
        if id.is_some_and(|id| id <= 0) {
            return Err("Invalid milestone selection.".into());
        }
        let milestone = save_milestone(
            &self.server()?,
            owner,
            repository,
            id,
            MilestoneDraft {
                title: title.into(),
                description,
                due_date,
            },
        )
        .await?;
        milestone
            .id
            .ok_or_else(|| "The saved milestone has no ID.".into())
    }

    pub async fn pull_filters(&self) -> Result<PullFilterOptions, GotchaError> {
        let server = self.server()?;
        let filter = self
            .state
            .lock()
            .unwrap()
            .preferences
            .pull_filters
            .get(&server.url)
            .cloned()
            .unwrap_or_default();
        Ok(pull_filter_options(
            &load_pull_milestones(&server).await?,
            &filter,
        ))
    }

    pub fn pull_filters_active(&self) -> Result<bool, GotchaError> {
        let server = self.server()?;
        let state = self.state.lock().unwrap();
        let filter = state
            .preferences
            .pull_filters
            .get(&server.url)
            .cloned()
            .unwrap_or_default();
        Ok(filter.is_active(&state.preferences.pull_status))
    }

    pub fn set_pull_filters(&self, milestone: String) -> Result<(), GotchaError> {
        let filter = PullFilter { milestone };
        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 = server.url.clone();
        if filter == PullFilter::default() {
            state.preferences.pull_filters.remove(&key);
        } else {
            state.preferences.pull_filters.insert(key, filter);
        }
        save_preferences(&state.preferences).map_err(Into::into)
    }

    pub async fn pulls(&self, page: u32) -> Result<PullListPage, GotchaError> {
        let server = self.server()?;
        let (status, filter) = {
            let state = self.state.lock().unwrap();
            (
                state.preferences.pull_status.clone(),
                state
                    .preferences
                    .pull_filters
                    .get(&server.url)
                    .cloned()
                    .unwrap_or_default(),
            )
        };
        let page = load_pulls(&server, &status, &filter.milestone, valid_page(page)?).await?;
        Ok(PullListPage {
            rows: pull_rows(&page.items),
            has_more: page.has_more,
        })
    }

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

    pub async fn commits(
        &self,
        owner: String,
        repository: String,
        branch: Option<String>,
        path: String,
        pages: u32,
    ) -> 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 path = (!path.is_empty()).then_some(path.as_str());
        let commits = match branch.as_deref() {
            Some(branch) => {
                load_branch_commits(&server, &owner, &repository, branch, path, pages.max(1))
                    .await?
            }
            None => {
                load_all_commits(&server, &owner, &repository, &branches, path, pages.max(1))
                    .await?
            }
        };
        Ok(commit_page(
            branches,
            &commits.items,
            branch.is_none(),
            commits.has_more,
        ))
    }

    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<RepositoryFilePage, GotchaError> {
        let data = load_repository_file(&self.server()?, &owner, &repository, &path).await?;
        Ok(repository_file_page(&path, data))
    }

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

fn validate_repository<'a>(
    owner: &'a str,
    repository: &'a str,
) -> Result<(&'a str, &'a str), GotchaError> {
    let owner = owner.trim();
    let repository = repository.trim();
    if owner.is_empty() || repository.is_empty() {
        return Err("Select a repository first.".into());
    }
    Ok((owner, repository))
}

fn valid_page(page: u32) -> Result<i32, GotchaError> {
    (page > 0)
        .then(|| i32::try_from(page).ok())
        .flatten()
        .ok_or_else(|| "Invalid page number.".into())
}

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