use std::{error::Error, io::Read};

use gotcha_gitea::{
    Client, CreateIssue, EditIssue, IssueQuery,
    models::{
        CreateMilestoneOption, CreatePullRequestOption, EditMilestoneOption, EditPullRequestOption,
        MergePullRequestOption,
    },
};
use serde::de::DeserializeOwned;

use crate::config::{RepositoryScope, Selection};

pub async fn run(
    command: &[String],
    selection: &Selection,
    client: &Client,
) -> Result<bool, Box<dyn Error>> {
    match command {
        [domain, action, arguments @ ..] if domain == "issue" && action == "list" => {
            let options = parse_issue_list(arguments)?;
            let scope = scope(selection, options.repository.as_deref())?;
            let issues = client
                .issues(
                    &scope,
                    &IssueQuery {
                        state: options.state,
                        labels: options.labels,
                        keyword: options.keyword,
                        kind: options.kind,
                        milestones: options.milestones,
                        from: options.from,
                        until: options.until,
                        author: options.author,
                        assignee: options.assignee,
                        mentions: options.mentions,
                        page: options.page,
                        limit: options.limit,
                    },
                )
                .await?;
            print_issues(&issues.items);
        }
        [domain, action, index] if domain == "issue" && action == "show" => {
            let scope = scope(selection, None)?;
            print_issue(&client.issue(&scope, number(index)?).await?);
        }
        [domain, action, index, repository] if domain == "issue" && action == "show" => {
            let scope = scope(selection, Some(repository))?;
            print_issue(&client.issue(&scope, number(index)?).await?);
        }
        [domain, action] if domain == "issue" && action == "create" => {
            create_issue(client, &scope(selection, None)?).await?;
        }
        [domain, action, repository] if domain == "issue" && action == "create" => {
            create_issue(client, &scope(selection, Some(repository))?).await?;
        }
        [domain, action, arguments @ ..] if domain == "issue" && action == "edit" => {
            let (indexes, repository) = issue_targets(arguments)?;
            edit_issues(client, &scope(selection, repository.as_deref())?, &indexes).await?;
        }
        [domain, action, arguments @ ..]
            if domain == "issue" && matches!(action.as_str(), "close" | "reopen") =>
        {
            let (indexes, repository) = issue_targets(arguments)?;
            let scope = scope(selection, repository.as_deref())?;
            set_issue_state(
                client,
                &scope,
                &indexes,
                if action == "close" { "closed" } else { "open" },
            )
            .await?;
        }
        [domain, action, index] if domain == "issue" && action == "delete" => {
            delete_issue(client, &scope(selection, None)?, number(index)?).await?;
        }
        [domain, action, index, repository] if domain == "issue" && action == "delete" => {
            delete_issue(client, &scope(selection, Some(repository))?, number(index)?).await?;
        }
        [domain, action, index] if domain == "issue" && action == "comments" => {
            list_comments(client, &scope(selection, None)?, number(index)?).await?;
        }
        [domain, action, index, repository] if domain == "issue" && action == "comments" => {
            list_comments(client, &scope(selection, Some(repository))?, number(index)?).await?;
        }
        [domain, action, index] if domain == "issue" && action == "comment" => {
            create_comment(client, &scope(selection, None)?, number(index)?).await?;
        }
        [domain, action, index, repository] if domain == "issue" && action == "comment" => {
            create_comment(client, &scope(selection, Some(repository))?, number(index)?).await?;
        }
        [domain, action] if domain == "milestone" && action == "list" => {
            list_milestones(client, &scope(selection, None)?).await?;
        }
        [domain, action, repository] if domain == "milestone" && action == "list" => {
            list_milestones(client, &scope(selection, Some(repository))?).await?;
        }
        [domain, action, id] if domain == "milestone" && action == "show" => {
            show_milestone(client, &scope(selection, None)?, id).await?;
        }
        [domain, action, id, repository] if domain == "milestone" && action == "show" => {
            show_milestone(client, &scope(selection, Some(repository))?, id).await?;
        }
        [domain, action] if domain == "milestone" && action == "create" => {
            create_milestone(client, &scope(selection, None)?).await?;
        }
        [domain, action, repository] if domain == "milestone" && action == "create" => {
            create_milestone(client, &scope(selection, Some(repository))?).await?;
        }
        [domain, action, id] if domain == "milestone" && action == "edit" => {
            edit_milestone(client, &scope(selection, None)?, id).await?;
        }
        [domain, action, id, repository] if domain == "milestone" && action == "edit" => {
            edit_milestone(client, &scope(selection, Some(repository))?, id).await?;
        }
        [domain, action, id] if domain == "milestone" && action == "delete" => {
            delete_milestone(client, &scope(selection, None)?, id).await?;
        }
        [domain, action, id, repository] if domain == "milestone" && action == "delete" => {
            delete_milestone(client, &scope(selection, Some(repository))?, id).await?;
        }
        [domain, action] if domain == "pull" && action == "list" => {
            list_pulls(client, &scope(selection, None)?).await?;
        }
        [domain, action, repository] if domain == "pull" && action == "list" => {
            list_pulls(client, &scope(selection, Some(repository))?).await?;
        }
        [domain, action, index] if domain == "pull" && action == "show" => {
            show_pull(client, &scope(selection, None)?, number(index)?).await?;
        }
        [domain, action, index, repository] if domain == "pull" && action == "show" => {
            show_pull(client, &scope(selection, Some(repository))?, number(index)?).await?;
        }
        [domain, action] if domain == "pull" && action == "create" => {
            create_pull(client, &scope(selection, None)?).await?;
        }
        [domain, action, repository] if domain == "pull" && action == "create" => {
            create_pull(client, &scope(selection, Some(repository))?).await?;
        }
        [domain, action, index] if domain == "pull" && action == "edit" => {
            edit_pull(client, &scope(selection, None)?, number(index)?).await?;
        }
        [domain, action, index, repository] if domain == "pull" && action == "edit" => {
            edit_pull(client, &scope(selection, Some(repository))?, number(index)?).await?;
        }
        [domain, action, index] if domain == "pull" && action == "merge" => {
            merge_pull(client, &scope(selection, None)?, number(index)?).await?;
        }
        [domain, action, index, repository] if domain == "pull" && action == "merge" => {
            merge_pull(client, &scope(selection, Some(repository))?, number(index)?).await?;
        }
        [domain, action, index]
            if domain == "pull" && matches!(action.as_str(), "commits" | "files" | "reviews") =>
        {
            pull_details(client, &scope(selection, None)?, number(index)?, action).await?;
        }
        [domain, action, index, repository]
            if domain == "pull" && matches!(action.as_str(), "commits" | "files" | "reviews") =>
        {
            pull_details(
                client,
                &scope(selection, Some(repository))?,
                number(index)?,
                action,
            )
            .await?;
        }
        [domain, ..] if matches!(domain.as_str(), "issue" | "milestone" | "pull") => {
            return Ok(false);
        }
        _ => return Ok(false),
    }
    Ok(true)
}

fn scope(selection: &Selection, value: Option<&str>) -> Result<RepositoryScope, Box<dyn Error>> {
    match value {
        Some(value) => Ok(RepositoryScope::parse(value)?),
        None => selection.repository.clone().ok_or_else(|| {
            "cannot infer a repository; run inside one or pass OWNER/REPOSITORY".into()
        }),
    }
}

fn number(value: &str) -> Result<i64, Box<dyn Error>> {
    let number: i64 = value.parse()?;
    if number < 1 {
        return Err("index must be a positive integer".into());
    }
    Ok(number)
}

fn read_stdin() -> Result<String, Box<dyn Error>> {
    let mut input = String::new();
    std::io::stdin().read_to_string(&mut input)?;
    if input.trim().is_empty() {
        return Err("stdin must not be empty".into());
    }
    Ok(input)
}

fn read_yaml<T: DeserializeOwned>() -> Result<T, Box<dyn Error>> {
    parse_yaml(&read_stdin()?)
}

fn parse_yaml<T: DeserializeOwned>(input: &str) -> Result<T, Box<dyn Error>> {
    Ok(serde_yaml::from_str(input)?)
}

async fn create_issue(client: &Client, scope: &RepositoryScope) -> Result<(), Box<dyn Error>> {
    let input: CreateIssueInput = read_yaml()?;
    print_issue(
        &client
            .create_issue(
                scope,
                CreateIssue {
                    option: input.issue,
                    label_names: input.label_names,
                    milestone_name: input.milestone_name,
                },
            )
            .await?,
    );
    Ok(())
}

async fn edit_issues(
    client: &Client,
    scope: &RepositoryScope,
    indexes: &[i64],
) -> Result<(), Box<dyn Error>> {
    let input: EditIssueInput = read_yaml()?;
    for issue in client
        .edit_issues(
            scope,
            indexes,
            EditIssue {
                option: input.issue,
                replace_labels: None,
                add_labels: input.add_labels,
                remove_labels: input.remove_labels,
                add_assignees: input.add_assignees,
                milestone_name: input.milestone_name,
            },
        )
        .await?
    {
        print_issue(&issue);
    }
    Ok(())
}

async fn set_issue_state(
    client: &Client,
    scope: &RepositoryScope,
    indexes: &[i64],
    state: &str,
) -> Result<(), Box<dyn Error>> {
    for issue in client.set_issue_state(scope, indexes, state).await? {
        print_issue(&issue);
    }
    Ok(())
}

async fn delete_issue(
    client: &Client,
    scope: &RepositoryScope,
    index: i64,
) -> Result<(), Box<dyn Error>> {
    client.delete_issue(scope, index).await?;
    println!("Deleted issue #{index}.");
    Ok(())
}

async fn list_comments(
    client: &Client,
    scope: &RepositoryScope,
    index: i64,
) -> Result<(), Box<dyn Error>> {
    let comments = client.issue_comments(scope, index).await?;
    print_comments(&comments);
    Ok(())
}

async fn create_comment(
    client: &Client,
    scope: &RepositoryScope,
    index: i64,
) -> Result<(), Box<dyn Error>> {
    let body = read_stdin()?.trim_end().to_owned();
    let comment = client.create_issue_comment(scope, index, body).await?;
    print_comments(&[comment]);
    Ok(())
}

async fn list_milestones(client: &Client, scope: &RepositoryScope) -> Result<(), Box<dyn Error>> {
    let milestones = client.milestones(scope).await?;
    print_milestones(&milestones);
    Ok(())
}

async fn show_milestone(
    client: &Client,
    scope: &RepositoryScope,
    id: &str,
) -> Result<(), Box<dyn Error>> {
    let milestone = client.milestone(scope, number(id)?).await?;
    print_milestone(&milestone);
    Ok(())
}

async fn create_milestone(client: &Client, scope: &RepositoryScope) -> Result<(), Box<dyn Error>> {
    let body: CreateMilestoneOption = read_yaml()?;
    let milestone = client.create_milestone(scope, body).await?;
    print_milestone(&milestone);
    Ok(())
}

async fn edit_milestone(
    client: &Client,
    scope: &RepositoryScope,
    id: &str,
) -> Result<(), Box<dyn Error>> {
    let milestone = client
        .edit_milestone(scope, id, read_yaml::<EditMilestoneOption>()?)
        .await?;
    print_milestone(&milestone);
    Ok(())
}

async fn delete_milestone(
    client: &Client,
    scope: &RepositoryScope,
    id: &str,
) -> Result<(), Box<dyn Error>> {
    client.delete_milestone(scope, id).await?;
    println!("Deleted milestone {id}.");
    Ok(())
}

async fn list_pulls(client: &Client, scope: &RepositoryScope) -> Result<(), Box<dyn Error>> {
    print_pulls(&client.repository_pulls(scope, "open", 1, 30).await?.items);
    Ok(())
}

async fn show_pull(
    client: &Client,
    scope: &RepositoryScope,
    index: i64,
) -> Result<(), Box<dyn Error>> {
    let pull = client.pull(scope, index).await?;
    print_pull(&pull);
    Ok(())
}

async fn create_pull(client: &Client, scope: &RepositoryScope) -> Result<(), Box<dyn Error>> {
    let body: CreatePullRequestOption = read_yaml()?;
    let pull = client.create_pull(scope, body).await?;
    print_pull(&pull);
    Ok(())
}

async fn edit_pull(
    client: &Client,
    scope: &RepositoryScope,
    index: i64,
) -> Result<(), Box<dyn Error>> {
    let pull = client
        .edit_pull(scope, index, read_yaml::<EditPullRequestOption>()?)
        .await?;
    print_pull(&pull);
    Ok(())
}

async fn merge_pull(
    client: &Client,
    scope: &RepositoryScope,
    index: i64,
) -> Result<(), Box<dyn Error>> {
    client
        .merge_pull(scope, index, read_yaml::<MergePullRequestOption>()?)
        .await?;
    println!("Merged pull request #{index}.");
    Ok(())
}

async fn pull_details(
    client: &Client,
    scope: &RepositoryScope,
    index: i64,
    action: &str,
) -> Result<(), Box<dyn Error>> {
    match action {
        "commits" => print_commits(&client.pull_commits(scope, index).await?),
        "files" => print_files(&client.pull_files(scope, index).await?),
        "reviews" => print_reviews(&client.pull_reviews(scope, index).await?),
        _ => unreachable!(),
    }
    Ok(())
}

mod arguments;
mod help;
mod output;

use arguments::*;
pub(crate) use help::{domain_help, subcommand_help};
use output::*;

#[cfg(test)]
mod tests;
