use std::error::Error;

use gotcha_gitea::models::{CreateIssueOption, EditIssueOption};
use serde::Deserialize;

use crate::config::RepositoryScope;

use super::number;

#[derive(Debug, PartialEq)]
pub(super) struct IssueListOptions {
    pub(super) repository: Option<String>,
    pub(super) state: String,
    pub(super) kind: String,
    pub(super) keyword: Option<String>,
    pub(super) labels: Option<String>,
    pub(super) milestones: Option<String>,
    pub(super) author: Option<String>,
    pub(super) assignee: Option<String>,
    pub(super) mentions: Option<String>,
    pub(super) from: Option<String>,
    pub(super) until: Option<String>,
    pub(super) page: i32,
    pub(super) limit: i32,
}

#[derive(Deserialize)]
pub(super) struct CreateIssueInput {
    #[serde(flatten)]
    pub(super) issue: CreateIssueOption,
    #[serde(default)]
    pub(super) label_names: Vec<String>,
    pub(super) milestone_name: Option<String>,
}

#[derive(Clone, Deserialize)]
pub(super) struct EditIssueInput {
    #[serde(flatten)]
    pub(super) issue: EditIssueOption,
    #[serde(default)]
    pub(super) add_labels: Vec<String>,
    #[serde(default)]
    pub(super) remove_labels: Vec<String>,
    #[serde(default)]
    pub(super) add_assignees: Vec<String>,
    pub(super) milestone_name: Option<String>,
}

impl Default for IssueListOptions {
    fn default() -> Self {
        Self {
            repository: None,
            state: "open".into(),
            kind: "issues".into(),
            keyword: None,
            labels: None,
            milestones: None,
            author: None,
            assignee: None,
            mentions: None,
            from: None,
            until: None,
            page: 1,
            limit: 30,
        }
    }
}

pub(super) fn parse_issue_list(arguments: &[String]) -> Result<IssueListOptions, Box<dyn Error>> {
    let mut options = IssueListOptions::default();
    let mut index = 0;
    while index < arguments.len() {
        let argument = arguments[index].as_str();
        let value = |index: &mut usize| -> Result<String, Box<dyn Error>> {
            *index += 1;
            arguments
                .get(*index)
                .cloned()
                .ok_or_else(|| format!("{argument} requires a value").into())
        };
        match argument {
            "--state" => options.state = value(&mut index)?,
            "-K" | "--kind" => options.kind = value(&mut index)?,
            "-k" | "--keyword" => options.keyword = Some(value(&mut index)?),
            "-L" | "--labels" => options.labels = Some(value(&mut index)?),
            "-m" | "--milestones" => options.milestones = Some(value(&mut index)?),
            "-A" | "--author" => options.author = Some(value(&mut index)?),
            "-a" | "--assignee" => options.assignee = Some(value(&mut index)?),
            "-M" | "--mentions" => options.mentions = Some(value(&mut index)?),
            "-F" | "--from" => options.from = Some(value(&mut index)?),
            "-u" | "--until" => options.until = Some(value(&mut index)?),
            "-p" | "--page" => options.page = positive_i32(&value(&mut index)?, "page")?,
            "--limit" | "--lm" => options.limit = positive_i32(&value(&mut index)?, "limit")?,
            unknown if unknown.starts_with('-') => {
                return Err(format!("unknown issue list option {unknown:?}").into());
            }
            repository if options.repository.is_none() => {
                RepositoryScope::parse(repository)?;
                options.repository = Some(repository.into());
            }
            value => return Err(format!("unexpected issue list argument {value:?}").into()),
        }
        index += 1;
    }
    if !matches!(options.state.as_str(), "open" | "closed" | "all") {
        return Err("--state must be open, closed, or all".into());
    }
    if !matches!(options.kind.as_str(), "issues" | "pulls" | "all") {
        return Err("--kind must be issues, pulls, or all".into());
    }
    Ok(options)
}

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

pub(super) fn issue_targets(
    arguments: &[String],
) -> Result<(Vec<i64>, Option<String>), Box<dyn Error>> {
    let mut indexes = Vec::new();
    let mut repository = None;
    for argument in arguments {
        if argument.contains('/') {
            if repository.is_some() {
                return Err("only one OWNER/REPOSITORY may be provided".into());
            }
            RepositoryScope::parse(argument)?;
            repository = Some(argument.clone());
        } else {
            indexes.push(number(argument)?);
        }
    }
    if indexes.is_empty() {
        return Err("at least one issue index is required".into());
    }
    Ok((indexes, repository))
}
