// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements.  See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership.  The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License.  You may obtain a copy of the License at
//
//   http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied.  See the License for the
// specific language governing permissions and limitations
// under the License.

use crate::error::{FtIndexError, Result};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;

#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum TokenizerKind {
    #[default]
    Default,
    Simple,
    Whitespace,
    Raw,
    Ngram,
    Jieba,
}

#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(default)]
pub struct TokenizerConfig {
    pub tokenizer: TokenizerKind,
    pub ngram_min_gram: usize,
    pub ngram_max_gram: usize,
    pub ngram_prefix_only: bool,
    pub jieba_search_mode: bool,
    pub jieba_ordinal_position: bool,
    pub lower_case: bool,
    pub max_token_length: usize,
    pub ascii_folding: bool,
    pub stem: bool,
    pub language: String,
    pub remove_stop_words: bool,
    pub stop_words: Vec<String>,
    pub with_position: bool,
}

impl Default for TokenizerConfig {
    fn default() -> Self {
        Self {
            tokenizer: TokenizerKind::Default,
            ngram_min_gram: 3,
            ngram_max_gram: 3,
            ngram_prefix_only: false,
            jieba_search_mode: true,
            jieba_ordinal_position: true,
            lower_case: true,
            max_token_length: 40,
            ascii_folding: true,
            stem: true,
            language: "english".to_string(),
            remove_stop_words: true,
            stop_words: Vec::new(),
            with_position: true,
        }
    }
}

impl TokenizerConfig {
    pub fn from_options(options: &HashMap<String, String>) -> Result<Self> {
        let mut config = Self::default();
        for (raw_key, value) in options {
            let key = raw_key
                .strip_prefix("fulltext.")
                .or_else(|| raw_key.strip_prefix("tantivy."))
                .unwrap_or(raw_key.as_str());
            let key = normalize_option_key(key);
            match key.as_str() {
                "tokenizer" => {
                    config.tokenizer = parse_tokenizer(value)?;
                }
                "ngram.min-gram" => {
                    config.ngram_min_gram = parse_usize(key, value)?;
                }
                "ngram.max-gram" => {
                    config.ngram_max_gram = parse_usize(key, value)?;
                }
                "ngram.prefix-only" => {
                    config.ngram_prefix_only = parse_bool(key, value)?;
                }
                "jieba.search-mode" => {
                    config.jieba_search_mode = parse_bool(key, value)?;
                }
                "jieba.ordinal-position" | "jieba.ordinal-position-mode" => {
                    config.jieba_ordinal_position = parse_bool(key, value)?;
                }
                "lower-case" => {
                    config.lower_case = parse_bool(key, value)?;
                }
                "max-token-length" => {
                    config.max_token_length = parse_usize(key, value)?;
                }
                "ascii-folding" => {
                    config.ascii_folding = parse_bool(key, value)?;
                }
                "stem" => {
                    config.stem = parse_bool(key, value)?;
                }
                "language" => {
                    config.language = value.trim().to_lowercase();
                }
                "remove-stop-words" => {
                    config.remove_stop_words = parse_bool(key, value)?;
                }
                "stop-words" => {
                    config.stop_words = value
                        .split(';')
                        .map(str::trim)
                        .filter(|word| !word.is_empty())
                        .map(ToOwned::to_owned)
                        .collect();
                }
                "with-position" => {
                    config.with_position = parse_bool(key, value)?;
                }
                _ => {}
            }
        }
        config.validate()?;
        Ok(config)
    }

    pub fn validate(&self) -> Result<()> {
        if self.ngram_min_gram == 0 {
            return invalid("ngram.min-gram", "must be positive");
        }
        if self.ngram_max_gram == 0 {
            return invalid("ngram.max-gram", "must be positive");
        }
        if self.ngram_min_gram > self.ngram_max_gram {
            return invalid("ngram.min-gram", "must not exceed ngram.max-gram");
        }
        if self.max_token_length == 0 {
            return invalid("max-token-length", "must be positive");
        }
        if !self.remove_stop_words && !self.stop_words.is_empty() {
            return invalid("stop-words", "requires remove-stop-words=true");
        }
        Ok(())
    }
}

fn parse_tokenizer(value: &str) -> Result<TokenizerKind> {
    let value = value.trim().to_lowercase();
    match value.as_str() {
        "default" => Ok(TokenizerKind::Default),
        "simple" => Ok(TokenizerKind::Simple),
        "whitespace" => Ok(TokenizerKind::Whitespace),
        "raw" => Ok(TokenizerKind::Raw),
        "ngram" => Ok(TokenizerKind::Ngram),
        "jieba" => Ok(TokenizerKind::Jieba),
        other => Err(FtIndexError::InvalidOption {
            key: "tokenizer".to_string(),
            message: format!("unsupported tokenizer '{other}'"),
        }),
    }
}

fn normalize_option_key(key: &str) -> String {
    key.trim().to_lowercase()
}

fn parse_usize(key: impl Into<String>, value: &str) -> Result<usize> {
    let key = key.into();
    value
        .trim()
        .parse()
        .map_err(|_| FtIndexError::InvalidOption {
            key,
            message: format!("expected unsigned integer, got '{value}'"),
        })
}

fn parse_bool(key: impl Into<String>, value: &str) -> Result<bool> {
    let key = key.into();
    match value.trim().to_lowercase().as_str() {
        "true" => Ok(true),
        "false" => Ok(false),
        _ => Err(FtIndexError::InvalidOption {
            key,
            message: format!("expected boolean, got '{value}'"),
        }),
    }
}

fn invalid<T>(key: &str, message: &str) -> Result<T> {
    Err(FtIndexError::InvalidOption {
        key: key.to_string(),
        message: message.to_string(),
    })
}
