from __future__ import annotations

import os
from dataclasses import dataclass
from pathlib import Path
from typing import ClassVar, Optional, cast

from colorama import Fore

from common import print_message
from common.yaml_helper import YamlHelper
from src.interface.python_project_dpsi import (
    MASTER_DATAS_FILE,
    METADATA_DIR,
    PRODUCT_FILE,
    TABLES_FILE,
)


class MetadataValidator(object):
    """Valida os arquivos YAML de metadados de todos os produtos do repositório.

    Centraliza as regras de validação de product.yaml, tables.yaml e
    master_datas.yaml para cada pasta de produto presente em metadata/.
    """

    @dataclass(frozen=True)
    class ValidationResult(object):
        """Resultado da execução de MetadataValidator.validate_all."""

        errors: list[str]
        total_folders: int
        passed_files: int
        failed_files: int

        @property
        def passed(self) -> bool:
            """Retorna True se nenhum erro foi encontrado.

            Returns:
                bool: True quando errors está vazio, False caso contrário.
            """
            return len(self.errors) == 0

    VALID_SYNC_MODES: ClassVar[list[str]] = [
        "incremental_append",
        "full_refresh_overwrite",
    ]
    VALID_SCHEMA_CASES: ClassVar[list[str]] = ["UPPER", "LOWER"]
    REQUIRED_FILES: ClassVar[list[str]] = [
        PRODUCT_FILE,
        TABLES_FILE,
        MASTER_DATAS_FILE,
    ]
    REPO_ROOT: ClassVar[Path] = Path.cwd()
    METADATA_ROOT: ClassVar[Path] = REPO_ROOT / METADATA_DIR

    _TABLE_REQUIRED_KEYS: ClassVar[list[str]] = [
        "name",
        "schema",
        "schema_case",
        "sync_mode",
        "unique_key",
        "fields",
    ]
    _MD_REQUIRED_KEYS: ClassVar[list[str]] = [
        "name",
        "dest_name",
        "unique_key",
        "sync_mode",
        "tables",
        "fields",
    ]

    def __init__(
        self, repo_root: Path | None = None, metadata_root: Path | None = None
    ) -> None:
        """Inicializa o validador com os caminhos do repositório.

        Args:
            repo_root (Path | None): Caminho para a raiz do repositório.
                Quando None, usa REPO_ROOT (Path.cwd()).
            metadata_root (Path | None): Caminho para a pasta de metadados.
                Quando None, usa METADATA_ROOT (REPO_ROOT / metadata/).
        """
        self._repo_root = repo_root or self.REPO_ROOT
        self._metadata_root = metadata_root or self.METADATA_ROOT

    @staticmethod
    def _check_non_empty_string(value: object, label: str) -> Optional[str]:
        """Valida que value é uma string não-nula, retornando a mensagem de erro ou None.

        Args:
            value (object): Valor a ser validado.
            label (str): Nome do campo, usado na mensagem de erro.

        Returns:
            Optional[str]: Mensagem de erro se value for None ou não for string;
                None se for válido.
        """
        if YamlHelper.assert_non_empty_string(value):
            return None
        if value is None:
            return f"'{label}' não pode ser nulo."
        return f"'{label}' deve ser uma string, tipo recebido: {type(value).__name__}."

    def _check_sql_path_exists(self, value: object, label: str) -> Optional[str]:
        """Verifica que o caminho aponta para um arquivo .sql existente.

        O caminho é resolvido em relação à raiz do repositório.

        Args:
            value (object): Caminho relativo ao arquivo SQL, deve terminar com .sql.
            label (str): Nome do campo (usado na mensagem de erro).

        Returns:
            Optional[str]: Mensagem de erro se value não for string, não terminar
                com .sql ou se o arquivo referenciado não existir; None se for válido.
        """
        if not isinstance(value, str):
            return (
                f"'{label}' deve ser uma string, tipo recebido: {type(value).__name__}."
            )
        if not value.endswith(".sql"):
            return f"'{label}' deve terminar com '.sql', valor recebido: '{value}'."
        full_path = os.path.join(self._repo_root, value)
        return (
            None
            if os.path.isfile(full_path)
            else f"O caminho '{label}' não existe: '{full_path}'."
        )

    @staticmethod
    def _check_resolve_product(
        item: dict[str, object], key: str, default: str
    ) -> Optional[str]:
        """Resolve item[key], atribuindo default se ausente/nulo, e valida o tipo.

        Se a chave estiver presente e não-nula, valida que o valor é uma string.
        Caso contrário, atribui default ao item.

        Args:
            item (dict[str, object]): Dicionário do item YAML que pode conter a chave key.
            key (str): Nome da chave a ser resolvida (geralmente "product").
            default (str): Valor padrão a ser usado quando key estiver ausente ou
                None (geralmente o nome do produto em maiúsculas).

        Returns:
            Optional[str]: Mensagem de erro se o valor existente não for uma string;
                None caso contrário.
        """
        if item.get(key) is None:
            item[key] = default
            return None
        if not YamlHelper.assert_string(item[key]):
            return f"'{key}' deve ser uma string, tipo recebido: {type(item[key]).__name__}."
        return None

    @staticmethod
    def _load_root(
        path: str, root_key: str, file_label: str
    ) -> tuple[object, Optional[str]]:
        """Carrega o YAML e valida a chave raiz única, retornando o conteúdo ou erro.

        Args:
            path (str): Caminho do arquivo YAML.
            root_key (str): Única chave raiz permitida.
            file_label (str): Nome do arquivo, usado nas mensagens de erro.

        Returns:
            tuple[object, Optional[str]]: Tupla (conteúdo sob root_key, mensagem de erro).
                Em caso de erro o conteúdo é None; caso contrário a mensagem é None.
        """
        raw = YamlHelper.load_yaml(path)
        if raw is None:
            return None, f"YAML inválido em '{path}'."
        data = YamlHelper.assert_single_root_key(raw, root_key)
        if data is None:
            keys = list(raw.keys()) if isinstance(raw, dict) else type(raw).__name__
            return None, (
                f"[{file_label}] A raiz deve conter apenas a chave '{root_key}', "
                f"encontrado: {keys}."
            )
        return data[root_key], None

    @staticmethod
    def _check_enum(value: object, label: str, allowed: list[str]) -> Optional[str]:
        """Valida que value (se string) pertence ao conjunto allowed.

        Args:
            value (object): Valor a ser validado.
            label (str): Nome do campo, usado na mensagem de erro.
            allowed (list[str]): Valores permitidos.

        Returns:
            Optional[str]: Mensagem de erro se value for string e não estiver em
                allowed; None caso contrário.
        """
        if isinstance(value, str) and not YamlHelper.assert_enum(value, allowed):
            return f"'{label}' deve ser um de {allowed}, valor recebido: '{value}'"
        return None

    @staticmethod
    def _check_unique_key(value: object, ctx: str) -> Optional[str]:
        """Valida o padrão do campo unique_key (somente se for string).

        Args:
            value (object): Valor de unique_key.
            ctx (str): Contexto para a mensagem de erro.

        Returns:
            Optional[str]: Mensagem de erro se inválido; None caso contrário.
        """
        if isinstance(value, str) and not YamlHelper.assert_pattern(
            value, r"^\w[\w,]*$"
        ):
            return (
                f"'{ctx}.unique_key' deve conter apenas letras, números, underscores e vírgulas "
                f"(padrão ^[\\w,]+$), valor recebido: '{value}'"
            )
        return None

    @staticmethod
    def _check_incremental_field(
        item: dict[str, object], ctx: str, sync_mode: object
    ) -> Optional[str]:
        """Valida incremental_field quando sync_mode é 'incremental_append'.

        Args:
            item (dict[str, object]): Item da tabela.
            ctx (str): Contexto para a mensagem de erro.
            sync_mode (object): Valor de sync_mode do item.

        Returns:
            Optional[str]: Mensagem de erro se sync_mode for 'incremental_append' e
                incremental_field estiver ausente/vazio; None caso contrário.
        """
        if sync_mode != "incremental_append":
            return None
        incremental_field = item.get("incremental_field")
        if (
            not incremental_field
            or not isinstance(incremental_field, str)
            or not incremental_field.strip()
        ):
            return (
                f"'{ctx}.incremental_field' é obrigatório e não pode ser nulo ou vazio "
                f"quando sync_mode é 'incremental_append'."
            )
        return None

    @staticmethod
    def validate_product(path: str, folder_name: str) -> list[str]:
        """Valida o arquivo product.yaml de um produto.

        Verifica a estrutura do arquivo, chaves obrigatórias, tipos e a
        consistência do campo name com o nome da pasta do produto. Todos os
        erros de campo são acumulados; apenas erros estruturais que impedem
        a inspeção dos campos (YAML inválido, raiz incorreta, valor não-mapeamento)
        provocam retorno antecipado.

        Args:
            path (str): Caminho absoluto para o arquivo product.yaml.
            folder_name (str): Nome da pasta do produto (sem o caminho completo).

        Returns:
            list[str]: Lista de mensagens de erro encontradas (vazia se válido).
        """
        product, err = MetadataValidator._load_root(path, "product", PRODUCT_FILE)
        if err:
            return [err]
        if not isinstance(product, dict):
            return ["[product.yaml] O valor de 'product' deve ser um mapeamento."]

        errors: list[str] = []
        _required = ["name", "bucket_suffix"]
        missing = [k for k in _required if k not in product]
        if missing:
            errors.append(
                f"Chave(s) obrigatória(s) ausente(s) {missing} em product.yaml > product."
            )
        for key in ["name", "bucket_suffix"]:
            err = MetadataValidator._check_non_empty_string(
                product.get(key), f"product.yaml > product.{key}"
            )
            if err:
                errors.append(err)

        expected_name = folder_name.upper()
        name = product.get("name")
        if isinstance(name, str) and name != expected_name:
            errors.append(
                f"[product.yaml] 'product.name' deve ser '{expected_name}' "
                f"(nome da pasta em maiúsculas), valor recebido: '{name}'."
            )

        if errors:
            for err in errors:
                print_message(f"  [FAIL] product.yaml: {err}", Fore.RED)
        else:
            print_message("  [OK] product.yaml", Fore.GREEN)
        return errors

    @staticmethod
    def _validate_table_fields(fields: object, ctx: str) -> list[str]:
        """Valida a lista fields de uma tabela, acumulando todos os erros.

        Args:
            fields (object): Valor do campo fields a ser validado (deve ser uma lista não-vazia de strings).
            ctx (str): Contexto da tabela pai, usado nas mensagens de erro.

        Returns:
            list[str]: Lista de mensagens de erro encontradas (vazia se válido).
        """
        if not isinstance(fields, list) or len(fields) == 0:
            return [f"[tables.yaml] '{ctx}.fields' deve ser uma lista não-vazia."]
        errors: list[str] = []
        for f_idx, f_value in enumerate(fields):
            f_ctx = f"{ctx}.fields[{f_idx}]"
            if not isinstance(f_value, str) or not f_value.strip():
                errors.append(
                    f"[tables.yaml] {f_ctx} deve ser uma string não-vazia, tipo recebido: {type(f_value).__name__}."
                )
        return errors

    def _validate_table_item(
        self, item: object, idx: int, folder_name: str
    ) -> list[str]:
        """Valida um item da lista de tabelas, acumulando todos os erros.

        Args:
            item (object): Item a ser validado (deve ser um dicionário com as chaves obrigatórias).
            idx (int): Índice do item na lista, usado no contexto das mensagens de erro.
            folder_name (str): Nome da pasta do produto, usado como fallback para o campo product.

        Returns:
            list[str]: Lista de mensagens de erro encontradas (vazia se válido).
        """
        ctx = f"tables.yaml > tables[{idx}]"
        if not isinstance(item, dict):
            return [f"[tables.yaml] Item no índice {idx} deve ser um mapeamento."]

        errors: list[str] = []
        missing = [k for k in self._TABLE_REQUIRED_KEYS if k not in item]
        if missing:
            errors.append(f"Chave(s) obrigatória(s) ausente(s) {missing} em {ctx}.")

        for key in ["name", "schema", "schema_case", "sync_mode", "unique_key"]:
            err = self._check_non_empty_string(item.get(key), f"{ctx}.{key}")
            if err:
                errors.append(err)

        sync_mode = item.get("sync_mode")
        errors.extend(
            err
            for err in (
                self._check_unique_key(item.get("unique_key"), ctx),
                self._check_enum(sync_mode, f"{ctx}.sync_mode", self.VALID_SYNC_MODES),
                self._check_enum(
                    item.get("schema_case"),
                    f"{ctx}.schema_case",
                    self.VALID_SCHEMA_CASES,
                ),
                self._check_incremental_field(item, ctx, sync_mode),
                self._check_resolve_product(item, "product", folder_name.upper()),
            )
            if err
        )
        for key in ["product", "incremental_field"]:
            item.setdefault(key, None)

        errors.extend(self._validate_table_fields(item.get("fields"), ctx))
        return errors

    def validate_table(
        self, path: str, folder_name: str
    ) -> tuple[list[dict[str, object]], list[str]]:
        """Valida o arquivo tables.yaml de um produto.

        Verifica que a lista de tabelas é não-vazia, que cada tabela possui
        as chaves obrigatórias com os tipos e valores corretos, e que cada
        campo na lista fields é uma string não-vazia. Cada tabela é validada
        independentemente e todos os erros (de todas as tabelas e de todos os
        campos de cada tabela) são acumulados e impressos inline.

        Args:
            path (str): Caminho absoluto para o arquivo tables.yaml.
            folder_name (str): Nome da pasta do produto, usado como fallback para
                o campo product de cada tabela.

        Returns:
            tuple[list[dict[str, object]], list[str]]: Tupla com a lista de tabelas
                parseadas (para validação cruzada de master_datas.yaml) e a lista de
                mensagens de erro encontradas (vazia se válido).
        """
        table_list, err = self._load_root(path, "tables", TABLES_FILE)
        if err:
            return [], [err]
        if not isinstance(table_list, list) or len(table_list) == 0:
            return [], [
                "[tables.yaml] 'tables' deve ser uma lista não-vazia (itens prefixados com '-')."
            ]

        errors: list[str] = []
        for idx, item in enumerate(table_list):
            item_errors = self._validate_table_item(item, idx, folder_name)
            for err in item_errors:
                print_message(f"    [FAIL] {err}", Fore.RED)
            errors.extend(item_errors)

        if not errors:
            print_message(
                f"  [OK] tables.yaml ({len(table_list)} tabela(s))", Fore.GREEN
            )
        return cast(list[dict[str, object]], table_list), errors

    def _validate_master_data_item_paths(
        self, item: dict[str, object], ctx: str
    ) -> list[str]:
        """Valida os caminhos SQL de um item de master_datas, acumulando erros.

        Args:
            item (dict[str, object]): Item de master_datas.
            ctx (str): Contexto para mensagens de erro.

        Returns:
            list[str]: Lista de mensagens de erro encontradas (vazia se válido).
        """
        errors: list[str] = []
        path_tenant = item.get("path_tenant")
        path_anonymous = item.get("path_anonymous")
        if not path_tenant and not path_anonymous:
            errors.append(
                f"[master_datas.yaml] {ctx}: pelo menos um de 'path_tenant' ou 'path_anonymous' deve ser preenchido."
            )
        for path_key, path_val in [
            ("path_tenant", path_tenant),
            ("path_anonymous", path_anonymous),
        ]:
            if path_val:
                err = self._check_sql_path_exists(path_val, f"{ctx}.{path_key}")
                if err:
                    errors.append(err)
        return errors

    def _validate_master_data_table(
        self,
        table: object,
        t_idx: int,
        ctx: str,
        item_product: str,
        folder_name: str,
        known_tables: list[dict[str, object]] | None,
    ) -> list[str]:
        """Valida uma tabela dentro de um item de master_datas, acumulando erros.

        Args:
            table (object): Tabela a ser validada.
            t_idx (int): Índice da tabela.
            ctx (str): Contexto para mensagens de erro.
            item_product (str): Produto do item pai.
            folder_name (str): Nome da pasta do produto.
            known_tables (list[dict[str, object]] | None): Lista de tabelas conhecidas do tables.yaml.

        Returns:
            list[str]: Lista de mensagens de erro encontradas (vazia se válido).
        """
        t_ctx = f"{ctx}.tables[{t_idx}]"
        if not isinstance(table, dict):
            return [f"[master_datas.yaml] {t_ctx} deve ser um mapeamento."]

        errors: list[str] = []
        missing = [k for k in ["name", "schema"] if k not in table]
        if missing:
            errors.append(f"Chave(s) obrigatória(s) ausente(s) {missing} em {t_ctx}.")

        name_err = self._check_non_empty_string(table.get("name"), f"{t_ctx}.name")
        schema_err = self._check_non_empty_string(
            table.get("schema"), f"{t_ctx}.schema"
        )
        errors.extend(err for err in (name_err, schema_err) if err)

        product_err = self._check_resolve_product(table, "product", item_product)
        if product_err:
            errors.append(product_err)

        # A verificação cruzada só é executada quando name/schema/product são válidos,
        # para evitar mensagens ruidosas como "tabela 'None' não encontrada".
        if not name_err and not schema_err and not product_err:
            cross_err = self._check_table_in_known_tables(
                table, t_ctx, folder_name, known_tables
            )
            if cross_err:
                errors.append(cross_err)
        return errors

    @staticmethod
    def _check_table_in_known_tables(
        table: dict[str, object],
        t_ctx: str,
        folder_name: str,
        known_tables: list[dict[str, object]] | None,
    ) -> Optional[str]:
        """Verifica se uma tabela está na lista de tabelas conhecidas.

        Args:
            table (dict[str, object]): Tabela a ser verificada.
            t_ctx (str): Contexto para mensagens de erro.
            folder_name (str): Nome da pasta do produto.
            known_tables (list[dict[str, object]] | None): Lista de tabelas conhecidas.

        Returns:
            Optional[str]: Mensagem de erro se a tabela não for encontrada na lista
                conhecida; None caso contrário.
        """
        if known_tables is None or table.get("product") != folder_name.upper():
            return None
        name = table.get("name")
        schema = table.get("schema")
        match = any(
            t.get("name") == name and t.get("schema") == schema for t in known_tables
        )
        if not match:
            return (
                f"[master_datas.yaml] {t_ctx}: tabela '{name}' com schema "
                f"'{schema}' (produto '{table.get('product')}') "
                f"não encontrada em tables.yaml."
            )
        return None

    def _validate_master_data_tables(
        self,
        item: dict[str, object],
        ctx: str,
        folder_name: str,
        known_tables: list[dict[str, object]] | None,
    ) -> list[str]:
        """Valida a lista de tabelas de um item de master_datas, acumulando erros.

        Args:
            item (dict[str, object]): Item de master_datas.
            ctx (str): Contexto para mensagens de erro.
            folder_name (str): Nome da pasta do produto.
            known_tables (list[dict[str, object]] | None): Lista de tabelas conhecidas.

        Returns:
            list[str]: Lista de mensagens de erro encontradas (vazia se válido).
        """
        tables = item.get("tables")
        if not isinstance(tables, list) or len(tables) == 0:
            return [f"[master_datas.yaml] '{ctx}.tables' deve ser uma lista não-vazia."]

        errors: list[str] = []
        for t_idx, table in enumerate(tables):
            errors.extend(
                self._validate_master_data_table(
                    table,
                    t_idx,
                    ctx,
                    cast(str, item.get("product")),
                    folder_name,
                    known_tables,
                )
            )
        return errors

    def _validate_master_data_fields(
        self,
        item: dict[str, object],
        ctx: str,
    ) -> list[str]:
        """Valida a lista de campos de um item de master_datas, acumulando erros.

        Args:
            item (dict[str, object]): Item de master_datas.
            ctx (str): Contexto para mensagens de erro.

        Returns:
            list[str]: Lista de mensagens de erro encontradas (vazia se válido).
        """
        fields = item.get("fields")
        if not isinstance(fields, list) or len(fields) == 0:
            return [f"[master_datas.yaml] '{ctx}.fields' deve ser uma lista não-vazia."]
        errors: list[str] = []
        for f_idx, f_item in enumerate(fields):
            f_ctx = f"{ctx}.fields[{f_idx}]"
            if not isinstance(f_item, dict):
                errors.append(f"[master_datas.yaml] {f_ctx} deve ser um mapeamento.")
                continue
            missing = [k for k in ["name", "type"] if k not in f_item]
            if missing:
                errors.append(
                    f"Chave(s) obrigatória(s) ausente(s) {missing} em {f_ctx}."
                )
            errors.extend(
                err
                for err in (
                    self._check_non_empty_string(f_item.get("name"), f"{f_ctx}.name"),
                    self._check_non_empty_string(f_item.get("type"), f"{f_ctx}.type"),
                )
                if err
            )
            if "description" in f_item and f_item["description"] is not None:
                err = self._check_non_empty_string(
                    f_item["description"], f"{f_ctx}.description"
                )
                if err:
                    errors.append(err)
        return errors

    def _validate_master_data_item_fields(
        self, item: dict[str, object], ctx: str
    ) -> list[str]:
        """Valida os campos obrigatórios de um item de master_datas, acumulando erros.

        Args:
            item (dict[str, object]): Item de master_datas.
            ctx (str): Contexto para mensagens de erro.

        Returns:
            list[str]: Lista de mensagens de erro encontradas (vazia se válido).
        """
        errors: list[str] = []
        missing = [k for k in self._MD_REQUIRED_KEYS if k not in item]
        if missing:
            errors.append(f"Chave(s) obrigatória(s) ausente(s) {missing} em {ctx}.")

        for key in ["name", "dest_name", "unique_key", "sync_mode"]:
            err = self._check_non_empty_string(item.get(key), f"{ctx}.{key}")
            if err:
                errors.append(err)

        unique_key = item.get("unique_key")
        if isinstance(unique_key, str) and not YamlHelper.assert_pattern(
            unique_key, r"^\w[\w,]*$"
        ):
            errors.append(
                f"'{ctx}.unique_key' deve conter apenas letras, números, underscores e vírgulas "
                f"(padrão ^[\\w,]+$), valor recebido: '{unique_key}'"
            )

        sync_mode = item.get("sync_mode")
        if isinstance(sync_mode, str) and not YamlHelper.assert_enum(
            sync_mode, self.VALID_SYNC_MODES
        ):
            errors.append(
                f"'{ctx}.sync_mode' deve ser um de {self.VALID_SYNC_MODES}, valor recebido: '{sync_mode}'"
            )

        dest_name = item.get("dest_name")
        if isinstance(dest_name, str):
            if not YamlHelper.assert_no_spaces(dest_name):
                errors.append(
                    f"'{ctx}.dest_name' não deve conter espaços, valor recebido: '{dest_name}'"
                )
            if not YamlHelper.assert_pattern(dest_name, r"^[a-z0-9_]+$"):
                errors.append(
                    f"'{ctx}.dest_name' deve conter apenas letras minúsculas, números e underscores "
                    f"(padrão ^[a-z0-9_]+$), valor recebido: '{dest_name}'"
                )
        return errors

    def _validate_master_data_item(
        self,
        item: object,
        idx: int,
        folder_name: str,
        known_tables: list[dict[str, object]] | None,
    ) -> list[str]:
        """Valida um item da lista de master_datas, acumulando todos os erros.

        Args:
            item (object): Item a ser validado.
            idx (int): Índice do item.
            folder_name (str): Nome da pasta do produto.
            known_tables (list[dict[str, object]] | None): Lista de tabelas conhecidas.

        Returns:
            list[str]: Lista de mensagens de erro encontradas (vazia se válido).
        """
        ctx = f"master_datas.yaml > master_datas[{idx}]"
        if not isinstance(item, dict):
            return [f"[master_datas.yaml] Item no índice {idx} deve ser um mapeamento."]

        errors: list[str] = []
        errors.extend(self._validate_master_data_item_fields(item, ctx))

        product_err = self._check_resolve_product(item, "product", folder_name.upper())
        if product_err:
            errors.append(product_err)
        for key in ["product", "path_tenant", "path_anonymous", "calendar"]:
            item.setdefault(key, None)

        errors.extend(self._validate_master_data_item_paths(item, ctx))
        errors.extend(
            self._validate_master_data_tables(item, ctx, folder_name, known_tables)
        )
        errors.extend(self._validate_master_data_fields(item, ctx))
        return errors

    def validate_master_data(
        self,
        path: str,
        folder_name: str,
        known_tables: list[dict[str, object]] | None = None,
    ) -> list[str]:
        """Valida o arquivo master_datas.yaml de um produto.

        Verifica que a lista de master datas é não-vazia, que cada entrada possui
        as chaves obrigatórias com tipos e valores corretos, que pelo menos um
        dos campos path_tenant ou path_anonymous está preenchido (e aponta
        para um arquivo .sql existente), e que a lista tables é válida. Cada
        entrada, tabela e campo são validados independentemente e TODOS os
        erros são acumulados e impressos inline.

        Args:
            path (str): Caminho absoluto para o arquivo master_datas.yaml.
            folder_name (str): Nome da pasta do produto, usado como fallback para
                o campo product de cada entrada.
            known_tables (list[dict[str, object]] | None): Lista de tabelas parseadas do
                tables.yaml do mesmo produto (retornada por validate_table). Quando
                fornecida, cada tabela referenciada cujo product coincida com o produto
                atual é verificada contra esta lista (chave: product + name + schema).
                Defaults to None.

        Returns:
            list[str]: Lista de mensagens de erro encontradas (vazia se válido).
        """
        md_list, err = self._load_root(path, "master_datas", MASTER_DATAS_FILE)
        if err:
            return [err]
        if not isinstance(md_list, list) or len(md_list) == 0:
            return [
                "[master_datas.yaml] 'master_datas' deve ser uma lista não-vazia (itens prefixados com '-')."
            ]

        errors: list[str] = []
        for idx, item in enumerate(md_list):
            item_errors = self._validate_master_data_item(
                item, idx, folder_name, known_tables
            )
            for err in item_errors:
                print_message(f"    [FAIL] {err}", Fore.RED)
            errors.extend(item_errors)

        if not errors:
            print_message(
                f"  [OK] master_datas.yaml ({len(md_list)} entrada(s))", Fore.GREEN
            )
        return errors

    def validate_folder(self, folder: str) -> tuple[list[str], int, int]:
        """Valida todos os arquivos YAML obrigatórios de uma pasta de produto.

        Executa a validação de cada arquivo (product.yaml, tables.yaml,
        master_datas.yaml) de forma independente: uma falha em um arquivo não
        interrompe a validação dos demais, e dentro de cada arquivo todos os
        erros são acumulados. Erros são impressos pelos próprios validadores.

        Args:
            folder (str): Caminho para a pasta do produto a ser validada.

        Returns:
            tuple[list[str], int, int]: Tupla com (lista de mensagens de erro,
                quantidade de arquivos que passaram, quantidade de arquivos que falharam).
        """
        folder = os.path.normpath(folder)
        folder_name = os.path.basename(folder)
        print_message(f"Validando: '{folder}' (produto: {folder_name.upper()})")
        print_message("  Ordem: product -> tables -> master_datas")

        missing = [
            f
            for f in self.REQUIRED_FILES
            if not os.path.isfile(os.path.join(folder, f))
        ]
        if missing:
            msg = f"Arquivo(s) obrigatório(s) ausente(s) em '{folder}': {missing}"
            print_message(f"  [FAIL] {msg}", Fore.RED)
            return [msg], 0, len(self.REQUIRED_FILES)

        errors: list[str] = []
        failed_files = 0

        product_errors = self.validate_product(
            os.path.join(folder, PRODUCT_FILE), folder_name
        )
        errors.extend(product_errors)
        failed_files += 1 if product_errors else 0

        table_list, table_errors = self.validate_table(
            os.path.join(folder, TABLES_FILE), folder_name
        )
        errors.extend(table_errors)
        failed_files += 1 if table_errors else 0

        # Só usa as tabelas para validação cruzada se tables.yaml estiver íntegro,
        # evitando comparar contra itens malformados.
        known_tables = table_list if not table_errors else None
        md_errors = self.validate_master_data(
            os.path.join(folder, MASTER_DATAS_FILE),
            folder_name,
            known_tables=known_tables,
        )
        errors.extend(md_errors)
        failed_files += 1 if md_errors else 0

        passed_files = len(self.REQUIRED_FILES) - failed_files
        return errors, passed_files, failed_files

    def validate_all(self) -> ValidationResult:
        """Escaneia todas as subpastas de metadata/ e valida cada produto.

        Cada arquivo (product.yaml, tables.yaml, master_datas.yaml) é validado
        de forma independente em todas as pastas, acumulando todos os erros.
        Erros em um arquivo não interrompem a validação dos demais.

        Returns:
            ValidationResult: Resultado com erros encontrados, total de pastas,
                contagem de arquivos que passaram e que falharam.
        """
        if not os.path.isdir(self._metadata_root):
            return self.ValidationResult(
                errors=[],
                total_folders=0,
                passed_files=0,
                failed_files=0,
            )

        folders = sorted(
            entry.path for entry in os.scandir(self._metadata_root) if entry.is_dir()
        )
        if not folders:
            return self.ValidationResult(
                errors=[],
                total_folders=0,
                passed_files=0,
                failed_files=0,
            )

        all_errors: list[str] = []
        total_passed = 0
        total_failed = 0

        for folder in folders:
            try:
                folder_errors, passed, failed = self.validate_folder(folder)
            except Exception as exc:
                folder_errors = [str(exc)]
                passed = 0
                failed = len(self.REQUIRED_FILES)
                print_message(f"  [ERROR] {exc}", Fore.RED)

            total_passed += passed
            total_failed += failed
            all_errors.extend(folder_errors)

            if folder_errors:
                print_message(
                    f"  [FAIL] {failed} arquivo(s) com erro(s), {passed} passou(aram).",
                    Fore.RED,
                )
                print_message(
                    "Verifique seus arquivos YAML e corrija os erros listados acima.",
                    Fore.RED,
                )
            else:
                print_message("  [PASS]", Fore.GREEN)

        return self.ValidationResult(
            errors=all_errors,
            total_folders=len(folders),
            passed_files=total_passed,
            failed_files=total_failed,
        )

    def main(self) -> ValidationResult:
        """Ponto de entrada do script de validação de metadados.

        Escaneia todas as subpastas de metadata/, executa a validação de
        cada pasta de produto e imprime um resumo ao final.

        Returns:
            ValidationResult: Resultado da validação com erros encontrados e contagens.
        """
        result = self.validate_all()

        total_files = result.passed_files + result.failed_files
        if total_files == 0:
            print_message(
                "Validação ignorada: nenhuma pasta/arquivo de metadados encontrado."
            )
            return result
        print_message("")
        print_message(f"Pastas   : {result.total_folders}")
        print_message(
            f"Arquivos : {total_files} verificados — "
            f"{result.passed_files} passou(aram), {result.failed_files} falhou(aram)"
        )
        return result
