from copy import deepcopy
from dataclasses import dataclass
from datetime import datetime
from pathlib import Path
import re
import shutil
from typing import List, Optional

import yaml

from lib.criptografia_senha import criptografar


CAMPOS_CRIPTOGRAFADOS_PADRAO = (
    "acessos.banco.senha",
    "acessos.usuarioAdministrador.senha",
    "acessos.servidorLinux.senha",
)

PREFIXO_SENHA_CRIPTOGRAFADA = "".join(chr(ord(c) + 1) for c in "{NOVA}")
CAMINHO_ITEM_REGEX = re.compile(r"([^\.\[\]]+)|\[(\d+)\]")


@dataclass
class ResultadoPreparacaoConfiguracao:
    destino: Path
    criado: bool
    backup: Optional[Path]
    versao_anterior: Optional[str]
    versao_modelo: Optional[str]
    variaveis_aplicadas: List[str]
    campos_criptografados: List[str]


def carregar_yaml(caminho):
    caminho = Path(caminho)

    with caminho.open("r", encoding="utf-8") as arquivo:
        return yaml.safe_load(arquivo) or {}


def salvar_yaml(caminho, configuracoes):
    caminho = Path(caminho)
    caminho.parent.mkdir(parents=True, exist_ok=True)

    with caminho.open("w", encoding="utf-8") as arquivo:
        yaml.safe_dump(
            configuracoes,
            arquivo,
            allow_unicode=True,
            sort_keys=False,
            default_flow_style=False,
        )


def mesclar_configuracoes(modelo, atual):
    if isinstance(modelo, dict) and isinstance(atual, dict):
        resultado = deepcopy(modelo)

        for chave, valor_atual in atual.items():
            if chave in resultado:
                resultado[chave] = mesclar_configuracoes(resultado[chave], valor_atual)
            else:
                resultado[chave] = deepcopy(valor_atual)

        return resultado

    return deepcopy(atual)


def atualizar_configuracao_pelo_modelo(modelo, atual):
    atual_com_versao_modelo = deepcopy(atual)
    atual_com_versao_modelo["versao"] = modelo.get("versao")

    return mesclar_configuracoes(modelo, atual_com_versao_modelo)


def carregar_env(caminho_env):
    caminho_env = Path(caminho_env)

    if not caminho_env.exists():
        return {}

    variaveis = {}

    with caminho_env.open("r", encoding="utf-8") as arquivo:
        for numero_linha, linha in enumerate(arquivo, start=1):
            linha = linha.strip()

            if not linha or linha.startswith("#"):
                continue

            if linha.startswith("export "):
                linha = linha[len("export "):].strip()

            if "=" not in linha:
                raise ValueError(f"Linha {numero_linha} invalida em {caminho_env}: esperado CHAVE=VALOR")

            chave, valor = linha.split("=", 1)
            chave = chave.strip()

            if not chave:
                raise ValueError(f"Linha {numero_linha} invalida em {caminho_env}: chave vazia")

            variaveis[chave] = _normalizar_valor_env(valor.strip())

    return variaveis


def aplicar_variaveis(configuracoes, variaveis):
    aplicadas = []

    for caminho, valor_bruto in variaveis.items():
        valor_atual = obter_valor_por_caminho(configuracoes, caminho)
        definir_valor_por_caminho(
            configuracoes,
            caminho,
            _converter_valor(valor_bruto, valor_atual),
        )
        aplicadas.append(caminho)

    return aplicadas


def criptografar_campos(configuracoes, campos_criptografados=CAMPOS_CRIPTOGRAFADOS_PADRAO):
    campos_alterados = []

    for campo in campos_criptografados:
        valor = obter_valor_por_caminho(configuracoes, campo)

        if valor in (None, ""):
            continue

        valor = str(valor)

        if senha_esta_criptografada(valor):
            continue

        definir_valor_por_caminho(configuracoes, campo, criptografar(valor))
        campos_alterados.append(campo)

    return campos_alterados


def preparar_configuracao(
    caminho_modelo,
    caminho_destino,
    caminho_env=None,
    campos_criptografados=CAMPOS_CRIPTOGRAFADOS_PADRAO,
    gerar_backup=True,
    dry_run=False,
):
    caminho_modelo = Path(caminho_modelo)
    caminho_destino = Path(caminho_destino)
    caminho_env = Path(caminho_env) if caminho_env else None

    modelo = carregar_yaml(caminho_modelo)
    criado = not caminho_destino.exists()
    atual = {} if criado else carregar_yaml(caminho_destino)
    versao_anterior = atual.get("versao") if isinstance(atual, dict) else None
    versao_modelo = modelo.get("versao")

    configuracoes = modelo if criado else atualizar_configuracao_pelo_modelo(modelo, atual)
    variaveis = carregar_env(caminho_env) if caminho_env else {}
    variaveis_aplicadas = aplicar_variaveis(configuracoes, variaveis)
    campos_alterados = criptografar_campos(configuracoes, campos_criptografados)

    backup = None

    if not dry_run:
        if gerar_backup and caminho_destino.exists():
            backup = _criar_backup(caminho_destino)

        salvar_yaml(caminho_destino, configuracoes)

    return ResultadoPreparacaoConfiguracao(
        destino=caminho_destino,
        criado=criado,
        backup=backup,
        versao_anterior=versao_anterior,
        versao_modelo=versao_modelo,
        variaveis_aplicadas=variaveis_aplicadas,
        campos_criptografados=campos_alterados,
    )


def senha_esta_criptografada(valor):
    return isinstance(valor, str) and valor.startswith(PREFIXO_SENHA_CRIPTOGRAFADA)


def obter_valor_por_caminho(configuracoes, caminho):
    atual = configuracoes

    for item in _dividir_caminho(caminho):
        if isinstance(item, int):
            if not isinstance(atual, list) or item >= len(atual):
                raise KeyError(f"Caminho inexistente no YAML: {caminho}")
            atual = atual[item]
            continue

        if not isinstance(atual, dict) or item not in atual:
            raise KeyError(f"Caminho inexistente no YAML: {caminho}")

        atual = atual[item]

    return atual


def definir_valor_por_caminho(configuracoes, caminho, valor):
    itens = _dividir_caminho(caminho)

    if not itens:
        raise KeyError("Caminho vazio")

    atual = configuracoes

    for item in itens[:-1]:
        if isinstance(item, int):
            if not isinstance(atual, list) or item >= len(atual):
                raise KeyError(f"Caminho inexistente no YAML: {caminho}")
            atual = atual[item]
            continue

        if not isinstance(atual, dict) or item not in atual:
            raise KeyError(f"Caminho inexistente no YAML: {caminho}")

        atual = atual[item]

    ultimo = itens[-1]

    if isinstance(ultimo, int):
        if not isinstance(atual, list) or ultimo >= len(atual):
            raise KeyError(f"Caminho inexistente no YAML: {caminho}")
        atual[ultimo] = valor
        return

    if not isinstance(atual, dict) or ultimo not in atual:
        raise KeyError(f"Caminho inexistente no YAML: {caminho}")

    atual[ultimo] = valor


def _dividir_caminho(caminho):
    itens = []

    for parte in caminho.split("."):
        if not parte:
            raise KeyError(f"Caminho invalido: {caminho}")

        posicao = 0

        for match in CAMINHO_ITEM_REGEX.finditer(parte):
            if match.start() != posicao:
                raise KeyError(f"Caminho invalido: {caminho}")

            nome, indice = match.groups()
            itens.append(int(indice) if indice is not None else nome)
            posicao = match.end()

        if posicao != len(parte):
            raise KeyError(f"Caminho invalido: {caminho}")

    return itens


def _normalizar_valor_env(valor):
    if len(valor) >= 2 and valor[0] == valor[-1] and valor[0] in ("'", '"'):
        return valor[1:-1]

    return valor


def _converter_valor(valor_bruto, valor_atual):
    if isinstance(valor_atual, bool):
        return _converter_bool(valor_bruto)

    if isinstance(valor_atual, int) and not isinstance(valor_atual, bool):
        return int(valor_bruto)

    if isinstance(valor_atual, float):
        return float(valor_bruto)

    if valor_bruto.lower() in ("null", "none"):
        return None

    return valor_bruto


def _converter_bool(valor):
    valores_verdadeiros = ("true", "1", "sim", "s", "yes", "y")
    valores_falsos = ("false", "0", "nao", "n", "no")
    valor_normalizado = valor.strip().lower()

    if valor_normalizado in valores_verdadeiros:
        return True

    if valor_normalizado in valores_falsos:
        return False

    raise ValueError(f"Valor booleano invalido: {valor}")


def _criar_backup(caminho_destino):
    timestamp = datetime.now().strftime("%Y%m%d-%H%M%S")
    backup = caminho_destino.with_name(f"{caminho_destino.name}.bak-{timestamp}")
    shutil.copy2(caminho_destino, backup)
    return backup
