use napi::bindgen_prelude::*;
use napi_derive::napi;

use citesleuth_core as core;

#[napi(object)]
pub struct Ids {
    pub doi: Option<String>,
    pub arxiv: Option<String>,
    pub isbn: Option<String>,
}

impl From<Ids> for core::Ids {
    fn from(value: Ids) -> Self {
        core::Ids {
            doi: value.doi,
            arxiv: value.arxiv,
            isbn: value.isbn,
        }
    }
}

impl From<core::Ids> for Ids {
    fn from(value: core::Ids) -> Self {
        Ids {
            doi: value.doi,
            arxiv: value.arxiv,
            isbn: value.isbn,
        }
    }
}

#[napi(object)]
pub struct ReferenceFields {
    pub title: Option<String>,
    pub authors: Option<Vec<String>>,
    pub year: Option<i32>,
    pub venue: Option<String>,
    pub ids: Option<Ids>,
}

impl From<ReferenceFields> for core::ReferenceFields {
    fn from(value: ReferenceFields) -> Self {
        core::ReferenceFields {
            title: value.title,
            authors: value.authors.unwrap_or_default(),
            year: value.year,
            venue: value.venue,
            ids: value.ids.map(core::Ids::from).unwrap_or_default(),
        }
    }
}

#[napi(object)]
pub struct Candidate {
    pub provider: Option<String>,
    pub provider_id: Option<String>,
    pub title: Option<String>,
    pub authors: Option<Vec<String>>,
    pub year: Option<i32>,
    pub venue: Option<String>,
    pub ids: Option<Ids>,
    pub url: Option<String>,
}

impl From<Candidate> for core::Candidate {
    fn from(value: Candidate) -> Self {
        core::Candidate {
            provider: value.provider,
            provider_id: value.provider_id,
            title: value.title,
            authors: value.authors.unwrap_or_default(),
            year: value.year,
            venue: value.venue,
            ids: value.ids.map(core::Ids::from).unwrap_or_default(),
            url: value.url,
        }
    }
}

impl From<core::Candidate> for Candidate {
    fn from(value: core::Candidate) -> Self {
        Candidate {
            provider: value.provider,
            provider_id: value.provider_id,
            title: value.title,
            authors: Some(value.authors),
            year: value.year,
            venue: value.venue,
            ids: Some(value.ids.into()),
            url: value.url,
        }
    }
}

#[napi(object)]
pub struct ScoreBreakdown {
    pub title: f64,
    pub authors: f64,
    pub year: f64,
    pub venue: f64,
}

impl From<core::ScoreBreakdown> for ScoreBreakdown {
    fn from(value: core::ScoreBreakdown) -> Self {
        ScoreBreakdown {
            title: value.title,
            authors: value.authors,
            year: value.year,
            venue: value.venue,
        }
    }
}

#[napi(object)]
pub struct ScoreResult {
    pub score: f64,
    pub breakdown: ScoreBreakdown,
}

#[napi(object)]
pub struct ScoreConfigInput {
    pub title_weight: Option<f64>,
    pub author_weight: Option<f64>,
    pub year_weight: Option<f64>,
    pub venue_weight: Option<f64>,
}

fn to_score_config(input: Option<ScoreConfigInput>) -> core::ScoreConfig {
    let mut config = core::ScoreConfig::new();
    if let Some(input) = input {
        if let Some(value) = input.title_weight {
            config.title_weight = value;
        }
        if let Some(value) = input.author_weight {
            config.author_weight = value;
        }
        if let Some(value) = input.year_weight {
            config.year_weight = value;
        }
        if let Some(value) = input.venue_weight {
            config.venue_weight = value;
        }
    }
    config
}

#[napi(object)]
pub struct MatchConfigInput {
    pub verified_threshold: Option<f64>,
    pub likely_threshold: Option<f64>,
    pub overwrite_threshold: Option<f64>,
    pub score_config: Option<ScoreConfigInput>,
}

fn to_match_config(input: Option<MatchConfigInput>) -> core::MatchConfig {
    let mut config = core::MatchConfig::new();
    if let Some(input) = input {
        if let Some(value) = input.verified_threshold {
            config.verified_threshold = value;
        }
        if let Some(value) = input.likely_threshold {
            config.likely_threshold = value;
        }
        if let Some(value) = input.overwrite_threshold {
            config.overwrite_threshold = value;
        }
        if let Some(score_config) = input.score_config {
            config.score_config = to_score_config(Some(score_config));
        }
    }
    config
}

#[napi(object)]
pub struct EntryResult {
    pub verdict: String,
    pub score: f64,
    pub reasons: Vec<String>,
    pub best_candidate: Option<Candidate>,
    pub breakdown: Option<ScoreBreakdown>,
}

impl From<core::EntryResult> for EntryResult {
    fn from(value: core::EntryResult) -> Self {
        EntryResult {
            verdict: value.verdict,
            score: value.score,
            reasons: value.reasons,
            best_candidate: value.best_candidate.map(Candidate::from),
            breakdown: value.breakdown.map(ScoreBreakdown::from),
        }
    }
}

#[napi(js_name = "normalizeTitle")]
pub fn normalize_title(title: String) -> String {
    core::normalize_title(&title)
}

#[napi(js_name = "normalizeVenue")]
pub fn normalize_venue(venue: String) -> String {
    core::normalize_venue(&venue)
}

#[napi(js_name = "normalizeAuthorName")]
pub fn normalize_author_name(name: String) -> String {
    core::normalize_author_name(&name)
}

#[napi(js_name = "authorFamilyNames")]
pub fn author_family_names(authors: Vec<String>) -> Vec<String> {
    core::author_family_names(&authors)
}

#[napi(js_name = "scoreCandidate")]
pub fn score_candidate(reference: ReferenceFields, candidate: Candidate, config: Option<ScoreConfigInput>) -> ScoreResult {
    let ref_fields: core::ReferenceFields = reference.into();
    let cand_fields: core::ReferenceFields = core::ReferenceFields {
        title: candidate.title.clone(),
        authors: candidate.authors.clone().unwrap_or_default(),
        year: candidate.year,
        venue: candidate.venue.clone(),
        ids: candidate.ids.clone().map(core::Ids::from).unwrap_or_default(),
    };
    let result = core::score_candidate(&ref_fields, &cand_fields, Some(&to_score_config(config)));
    ScoreResult {
        score: result.score,
        breakdown: result.breakdown.into(),
    }
}

#[napi(js_name = "matchReference")]
pub fn match_reference(reference: ReferenceFields, candidates: Vec<Candidate>, config: Option<MatchConfigInput>) -> EntryResult {
    let reference: core::ReferenceFields = reference.into();
    let candidates: Vec<core::Candidate> = candidates.into_iter().map(core::Candidate::from).collect();
    let result = core::match_reference(&reference, &candidates, Some(&to_match_config(config)));
    result.into()
}
