import io
import logging
import os
import secrets
import string
import sys
from json import loads

import torch
import yaml
from typing import List, Union, Optional, Dict, Type
from pydantic import BaseModel, Field, ValidationError, field_validator

from contants import ModelType

JSON_AS_ASCII = False
MAX_CONTENT_LENGTH = 5242880

# Absolute path of vits-simple-api (current program root path)
BASE_DIR: str = os.path.dirname(os.path.realpath(__file__))
# Configuration file path
CONFIG_PATH: str = os.path.join(BASE_DIR, "config.yaml")

# WTForms CSRF
SECRET_KEY: str = secrets.token_hex(16)


def update_nested_dict(original, updates):
    for key, value in updates.items():
        if isinstance(value, dict) and key in original:
            update_nested_dict(original[key], value)
        else:
            original[key] = value


class VitsConfig(BaseModel):
    id: int = 0
    format: str = "wav"
    lang: str = "auto"
    length: float = 1.0
    noise: float = 0.33
    noisew: float = 0.4
    segment_size: int = 50
    use_streaming: bool = False


class W2V2VitsConfig(BaseModel):
    id: int = 0
    format: str = "wav"
    lang: str = "auto"
    length: float = 1.0
    noise: float = 0.33
    noisew: float = 0.4
    segment_size: int = 50
    emotion: int = 0


class HuBertVitsConfig(BaseModel):
    id: int = 0
    format: str = "wav"
    length: float = 1.0
    noise: float = 0.33
    noisew: float = 0.4


class BertVits2Config(BaseModel):
    id: int = 0
    speaker: Optional[str] = None
    format: str = "wav"
    lang: str = "auto"
    length: float = 1.0
    noise: float = 0.33
    noisew: float = 0.4
    segment_size: int = 50
    sdp_ratio: float = 0.2
    emotion: int = 0
    text_prompt: str = "Happy"
    style_text: Optional[str] = None
    style_weight: float = 0.7
    use_streaming: bool = False
    torch_data_type: Optional[str] = None


class GPTSoVitsPreset(BaseModel):
    refer_wav_path: Optional[str] = None
    prompt_text: Optional[str] = None
    prompt_lang: str = "auto"


class GPTSoVitsConfig(BaseModel):
    hz: int = 50
    is_half: bool = False
    id: int = 0
    lang: str = "auto"
    format: str = "wav"
    segment_size: int = 30
    top_k: int = 5
    top_p: float = 1.0
    temperature: float = 1.0
    use_streaming: bool = False
    batch_size: int = 5
    speed: float = 1.0
    seed: int = -1
    presets: Dict[str, GPTSoVitsPreset] = Field(default_factory=lambda: {"default": GPTSoVitsPreset(),
                                                                         "default2": GPTSoVitsPreset()})


class Reader(BaseModel):
    model_type: str = "VITS"
    id: int = 0
    preset: str = "default"


class ReadingConfig(BaseModel):
    interlocutor: Reader = Reader()
    narrator: Reader = Reader()


class ResourcePathsConfig(BaseModel):
    chinese_roberta_wwm_ext_large: str = "bert/chinese-roberta-wwm-ext-large"
    bert_base_japanese_v3: str = "bert/bert-base-japanese-v3"
    bert_large_japanese_v2: str = "bert/bert-large-japanese-v2"
    deberta_v2_large_japanese: str = "bert/deberta-v2-large-japanese"
    deberta_v3_large: str = "bert/deberta-v3-large"
    deberta_v2_large_japanese_char_wwm: str = "bert/deberta-v2-large-japanese-char-wwm"
    wav2vec2_large_robust_12_ft_emotion_msp_dim: str = "emotional/wav2vec2-large-robust-12-ft-emotion-msp-dim"
    clap_htsat_fused: str = "emotional/clap-htsat-fused"
    erlangshen_MegatronBert_1_3B_Chinese: str = "bert/Erlangshen-MegatronBert-1.3B-Chinese"
    vits_chinese_bert: str = "bert/vits_chinese_bert"
    hubert_soft_0d54a1f4: str = "hubert/hubert_soft/hubert-soft-0d54a1f4.pt"
    dimensional_emotion_npy: Union[str, List[str]] = "emotional/dimensional_emotion_npy"
    dimensional_emotion_model: str = "emotional/dimensional_emotion_model/models.yaml"
    g2pw_model: str = "G2PWModel"
    chinese_hubert_base: str = "hubert/chinese_hubert_base"


class BaseModelConfig(BaseModel):
    model_type: Optional[str]

    class Config:
        protected_namespaces = ()


class VITSModelConfig(BaseModelConfig):
    vits_path: str = None
    config_path: str = None
    dynamic_loading: Optional[bool] = False


class W2V2VITSModelConfig(BaseModelConfig):
    vits_path: str = None
    config_path: str = None


class HuBertVITSModelConfig(BaseModelConfig):
    vits_path: str = None
    config_path: str = None


class BertVITS2ModelConfig(BaseModelConfig):
    vits_path: str = None
    config_path: str = None


class GPTSoVITSModelConfig(BaseModelConfig):
    vits_path: str = None
    t2s_path: str = None


MODEL_TYPE_MAP: Dict[str, Type[BaseModelConfig]] = {
    ModelType.VITS: VITSModelConfig,
    ModelType.W2V2_VITS: W2V2VITSModelConfig,
    ModelType.HUBERT_VITS: HuBertVITSModelConfig,
    ModelType.BERT_VITS2: BertVITS2ModelConfig,
    ModelType.GPT_SOVITS: GPTSoVITSModelConfig
}


class TTSModelConfig(BaseModel):
    models_dir: str = "models"
    auto_load: bool = True
    tts_models: List[Union[
        VITSModelConfig,
        W2V2VITSModelConfig,
        HuBertVITSModelConfig,
        BertVITS2ModelConfig,
        GPTSoVITSModelConfig,
    ]] = Field(default_factory=list)

    @classmethod
    def recognition_model_type_by_config(self, config: dict) -> str:
        symbols = config.get("symbols", None)
        emotion_embedding = config["data"].get("emotion_embedding", False)

        if "use_spk_conditioned_encoder" in config["model"]:
            model_type = ModelType.BERT_VITS2
            return model_type

        if symbols != None:
            if not emotion_embedding:
                mode_type = ModelType.VITS
            else:
                mode_type = ModelType.W2V2_VITS
        else:
            mode_type = ModelType.HUBERT_VITS

        return mode_type

    @field_validator('tts_models', mode="before")
    def infer_model_type(cls, v):
        result = []
        for model in v:
            if 'model_type' not in model:
                if 'vits_path' in model and 'config_path' in model:
                    with open(model["config_path"], 'r', encoding='utf-8') as f:
                        data = loads(f.read())
                    model['model_type'] = cls.recognition_model_type_by_config(data)
                elif 'vits_path' in model and 't2s_path' in model:
                    model['model_type'] = ModelType.GPT_SOVITS

            model_class = MODEL_TYPE_MAP[model['model_type']]
            result.append(model_class(**model))
        return result

    def add_model(self, model_config: BaseModelConfig):
        if not isinstance(model_config, BaseModelConfig):
            raise TypeError("model_config must be an instance of BaseModelConfig")

        model_class = MODEL_TYPE_MAP.get(model_config.model_type)
        if model_class is None:
            raise ValueError(f"Unknown model_type: {model_config.model_type}")

        self.tts_models.append(model_class(**model_config.model_dump()))

    def update_tts_models(self, tts_models: list):
        self.tts_models = []
        for item in tts_models:
            tts_model = item["tts_model"]
            model_type = tts_model.get("model_type")

            if model_type:
                model_type = model_type.upper().replace("_", "-")
            else:
                if tts_model.get("t2s_path"):
                    model_type = ModelType.GPT_SOVITS
                else:
                    with open(tts_model["config_path"], 'r', encoding='utf-8') as f:
                        data = f.read()
                    model_type = self.recognition_model_type(loads(data))
            model_class = MODEL_TYPE_MAP.get(ModelType(model_type))
            if model_class is not None:
                try:
                    model_instance = model_class.model_validate(tts_model)
                    self.tts_models.append(model_instance)
                except ValidationError as e:
                    logging.error(f"Validation error for item {tts_model}: {e}")
            else:
                logging.error(f"Unknown model_type in data: {model_type}")


class HttpService(BaseModel):
    host: str = "0.0.0.0"
    port: int = 23456
    debug: bool = False
    origins: str = "*"


class LogConfig(BaseModel):
    logs_path: str = "logs"
    logs_backup_count: int = 30
    logging_level: str = "DEBUG"


class APIKey(BaseModel):
    key: str = Field(
        default_factory=lambda: ''.join(secrets.choice(string.ascii_letters + string.digits) for _ in range(24)))
    enabled: bool = True


class System(BaseModel):
    device: str = Field(default_factory=lambda: str(
        "cuda" if torch.cuda.is_available() else "mps" if torch.backends.mps.is_available() else "cpu"))
    upload_folder: str = "upload"
    cache_path: str = "cache"
    clean_interval_seconds: int = 3600
    cache_audio: bool = False
    api_key_enabled: bool = False
    api_keys: List[APIKey] = Field(default_factory=lambda: [APIKey() for _ in range(5)])
    is_admin_enabled: bool = True
    admin_route: str = '/admin'
    data_path: str = "data"


class LanguageIdentification(BaseModel):
    language_identification_library: str = "langid"
    espeak_library: Optional[str] = r"C:/Program Files/eSpeak NG/libespeak-ng.dll" if "win" in sys.platform else ""
    language_automatic_detect: List[str] = Field(default_factory=list)
    split_pattern: str = r'[\!\"\#\$\%\&\'\(\)\*\+\,\-\.\/\:\;\<\>\=\?\@\[\]\{\}\\\\\^\_\`' \
                         r'\！？。＂＃＄％＆＇（）＊＋，－／：；＜＝＞＠［＼］＾＿｀｛｜｝～｟｠｢｣､、〃》「」' \
                         r'『』【】〔〕〖〗〘〙〚〛〜〝〞〟〰〾〿–—‘\'\‛\“\”\„\‟…‧﹏.]+'


class Polyphonic(BaseModel):
    dict_path: str = "polyphonic.yaml"


class User(BaseModel):
    id: int = 0
    username: str = Field(
        default_factory=lambda: ''.join(secrets.choice(string.ascii_letters + string.digits) for _ in range(8)))
    password: str = Field(
        default_factory=lambda: ''.join(secrets.choice(string.ascii_letters + string.digits) for _ in range(16)))

    def is_authenticated(self):
        return True

    def is_active(self):
        return True

    def is_anonymous(self):
        return False

    def get_id(self):
        return str(self.id)


class NgrokConfig(BaseModel):
    auth_token: Optional[str] = None


class Config(BaseModel):
    http_service: HttpService = HttpService()
    ngrok_config: NgrokConfig = NgrokConfig()
    resource_paths_config: ResourcePathsConfig = ResourcePathsConfig()
    tts_model_config: TTSModelConfig = TTSModelConfig()
    admin: User = User()
    system: System = System()
    log_config: LogConfig = LogConfig()
    language_identification: LanguageIdentification = LanguageIdentification()
    polyphonic: Polyphonic = Polyphonic()
    reading_config: ReadingConfig = ReadingConfig()
    vits_config: VitsConfig = VitsConfig()
    w2v2_vits_config: W2V2VitsConfig = W2V2VitsConfig()
    hubert_vits_config: HuBertVitsConfig = HuBertVitsConfig()
    bert_vits2_config: BertVits2Config = BertVits2Config()
    gpt_sovits_config: GPTSoVitsConfig = GPTSoVitsConfig()

    @staticmethod
    def load_config(file_path: str):
        if not os.path.exists(file_path):
            config = Config()
            save_config_to_yaml(config)
            return config

        with open(file_path, 'r', encoding='utf-8') as file:
            config_data = yaml.safe_load(file)

        if config_data:
            try:
                config = Config(**config_data)
            except ValidationError as e:
                logging.error(f"Config validation error: {e}")
                config = Config()  # Load defaults
                for error in e.errors():
                    field = error['loc'][0]
                    if field in config.__annotations__:
                        default_value = getattr(Config, field, None)
                        if default_value is not None:
                            # Apply default value
                            setattr(config, field, default_value)
        else:
            config = Config()

        save_config_to_yaml(config)

        return config

    def update_config(self, update_data: Dict):
        try:
            new_config_data = self.model_dump()

            update_nested_dict(new_config_data, update_data)

            updated_config = Config(**new_config_data)

            save_config_to_yaml(updated_config)

            self.__dict__.update(updated_config.__dict__)

            return self
        except ValidationError as e:
            logging.error(f"Config validation error: {e}")
            return self


def save_config_to_yaml(config: Config):
    temp_file = io.StringIO()

    yaml.safe_dump(config.model_dump(), temp_file, allow_unicode=True, sort_keys=False)

    data = temp_file.getvalue()
    temp_file.close()

    with open(CONFIG_PATH, 'w', encoding='utf-8') as file:
        file.write(data)


config = Config.load_config(CONFIG_PATH)
