from __future__ import annotations

import dataclasses
import json
import os
import sys
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any, Callable, Optional, TypeVar, cast

import requests
from colorama import Fore

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

_T = TypeVar("_T")


class ApiError(Exception):
    """Exceção lançada quando a API retorna um status HTTP de erro (4xx/5xx)."""

    def __init__(self, method: str, url: str, status_code: int, body: str) -> None:
        """Inicializa ApiError com os detalhes da requisição que falhou.

        Args:
            method: Método HTTP utilizado (ex: "GET", "POST").
            url: URL completa da requisição.
            status_code: Código de status HTTP retornado.
            body: Corpo da resposta de erro.
        """
        self.status_code = status_code
        super().__init__(
            f"HTTP {status_code} ao chamar {method} {url} — resposta: {body}."
            " Entre em contato com o time de dados para verificar este problema."
        )


@dataclass
class _ProductData(object):
    """Dados de um produto lidos de product.yaml."""

    name: str = field(metadata={"folder_default": True})
    bucket_suffix: str = ""

    @classmethod
    def from_yaml(cls, raw: dict[str, object], folder_name: str) -> _ProductData:
        """Instancia _ProductData a partir de um dicionário YAML.

        Args:
            raw (dict[str, object]): Dicionário com os dados do produto lidos do YAML.
            folder_name (str): Nome da pasta do produto, usado como fallback para o campo name.

        Returns:
            _ProductData: Instância de _ProductData preenchida com os dados do YAML.
        """
        return ApiSync._build_from_yaml(cls, raw, folder_name)

    def to_api_payload(self) -> dict[str, object]:
        """Serializa o produto para o formato de payload esperado pela API.

        Returns:
            dict[str, object]: Dicionário com os campos renomeados conforme o contrato da API.
        """
        return ApiSync._to_api_payload(self)


@dataclass
class _TableData(object):
    """Dados de uma tabela lidos de tables.yaml."""

    name: str
    product: str = field(metadata={"folder_default": True, "api_name": "product_name"})
    schema: str = field(metadata={"api_name": "schema_name"})
    schema_case: str = ""
    sync_mode: str = ""
    unique_key: str = ""
    fields: list[str] = field(default_factory=list)
    incremental_field: Optional[str] = None

    @classmethod
    def from_yaml(cls, raw: dict[str, object], folder_name: str) -> _TableData:
        """Instancia _TableData a partir de um dicionário YAML.

        Args:
            raw (dict[str, object]): Dicionário com os dados da tabela lidos do YAML.
            folder_name (str): Nome da pasta do produto, usado como fallback para o campo product.

        Returns:
            _TableData: Instância de _TableData preenchida com os dados do YAML.
        """
        return ApiSync._build_from_yaml(cls, raw, folder_name)

    def to_api_payload(self) -> dict[str, object]:
        """Serializa a tabela para o formato de payload esperado pela API.

        Returns:
            dict[str, object]: Dicionário com os campos renomeados conforme o contrato da API.
        """
        return ApiSync._to_api_payload(self)


@dataclass
class _MasterDataTableRef(object):
    """Referência a uma tabela associada a um master data."""

    name: str
    schema: str = field(metadata={"api_name": "schema_name"})
    product: str = field(metadata={"folder_default": True, "api_name": "product_name"})

    @classmethod
    def from_yaml(
        cls, raw: dict[str, object], folder_name: str = ""
    ) -> _MasterDataTableRef:
        """Instancia _MasterDataTableRef a partir de um dicionário YAML.

        Args:
            raw (dict[str, object]): Dicionário com os dados da referência de tabela lidos do YAML.
            folder_name (str): Nome da pasta do produto, usado como fallback para o campo product.

        Returns:
            _MasterDataTableRef: Instância de _MasterDataTableRef preenchida com os dados do YAML.
        """
        return ApiSync._build_from_yaml(cls, raw, folder_name)

    def to_api_payload(self) -> dict[str, object]:
        """Serializa a referência de tabela para o formato esperado pela API.

        Returns:
            dict[str, object]: Dicionário com os campos renomeados conforme o contrato da API.
        """
        return ApiSync._to_api_payload(self)


@dataclass
class _MasterDataFieldRef(object):
    """Referência a um campo associado a um master data."""

    name: str
    type: str
    description: Optional[str] = None

    @classmethod
    def from_yaml(cls, raw: dict) -> _MasterDataFieldRef:
        """Instancia _MasterDataFieldRef a partir de um dicionário YAML.

        Args:
            raw (dict): Dicionário com os dados do field lidos do YAML.

        Returns:
            _MasterDataFieldRef: Instância de _MasterDataFieldRef preenchida com os dados do YAML.
        """
        return ApiSync._build_from_yaml(cls, raw)

    def to_api_payload(self) -> dict[str, Any]:
        """Serializa o field para o formato de payload esperado pela API.

        Returns:
            dict[str, Any]: Dicionário com os campos do field.
        """
        return ApiSync._to_api_payload(self)


@dataclass
class _MasterDataData(object):
    """Dados de um master data lidos de master_datas.yaml."""

    name: str
    product: str = field(metadata={"folder_default": True, "api_name": "product_name"})
    dest_name: str = ""
    unique_key: str = ""
    sync_mode: str = ""
    tables: list = field(default_factory=list)
    fields: list = field(default_factory=list)
    path_tenant: Optional[str] = field(
        default=None, metadata={"api_name": "url_sql_tenant"}
    )
    path_anonymous: Optional[str] = field(
        default=None, metadata={"api_name": "url_sql_anonymous"}
    )
    calendar: Optional[str] = field(default=None, metadata={"api_name": "d_calendar"})

    @classmethod
    def from_yaml(cls, raw: dict[str, object], folder_name: str) -> _MasterDataData:
        """Instancia _MasterDataData a partir de um dicionário YAML.

        Deserializa recursivamente a lista tables em instâncias de _MasterDataTableRef,
        propagando o product do master data como padrão para as tabelas filhas.

        Args:
            raw (dict[str, object]): Dicionário com os dados do master data lidos do YAML.
            folder_name (str): Nome da pasta do produto, usado como fallback para o campo product.

        Returns:
            _MasterDataData: Instância de _MasterDataData preenchida com os dados do YAML.
        """
        default_product = raw.get("product", folder_name.upper())
        raw_with_tables_and_fields = {
            **raw,
            "tables": [
                _MasterDataTableRef.from_yaml(t, default_product)
                for t in raw.get("tables", [])
            ],
            "fields": [_MasterDataFieldRef.from_yaml(f) for f in raw.get("fields", [])],
        }
        return ApiSync._build_from_yaml(cls, raw_with_tables_and_fields, folder_name)

    def to_api_payload(self) -> dict[str, object]:
        """Serializa o master data para o formato de payload esperado pela API.

        Returns:
            dict[str, object]: Dicionário com os campos renomeados conforme o contrato da API.
        """
        return ApiSync._to_api_payload(self)


class _HttpClient(object):
    """Cliente HTTP com autenticação Bearer para a API DPSI Control Flex."""

    def __init__(
        self,
        base_url: str,
        auth_url: str,
        access_key: str,
        secret: str,
        tenant_name: str,
    ) -> None:
        """Inicializa o cliente obtendo o Bearer token e configurando a sessão.

        Args:
            base_url (str): URL base da API. Barras finais são removidas automaticamente.
            auth_url (str): URL do endpoint de autenticação.
            access_key (str): Chave de acesso da integração.
            secret (str): Segredo da integração.
            tenant_name (str): Nome do tenant Senior X.

        Raises:
            ApiError: Se a autenticação retornar status 4xx/5xx.
        """
        self._base_url = base_url.rstrip("/")
        token = self._fetch_token(auth_url, access_key, secret, tenant_name)
        self._session = requests.Session()
        self._session.headers.update(
            {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
        )

    @staticmethod
    def _fetch_token(
        auth_url: str, access_key: str, secret: str, tenant_name: str
    ) -> str:
        """Autentica na plataforma Senior X e retorna o Bearer token.

        Args:
            auth_url (str): URL do endpoint de autenticação.
            access_key (str): Chave de acesso da integração.
            secret (str): Segredo da integração.
            tenant_name (str): Nome do tenant Senior X.

        Returns:
            str: Bearer token pronto para uso nas requisições.

        Raises:
            ApiError: Se o endpoint de autenticação retornar 4xx/5xx.
        """
        response = requests.post(
            auth_url,
            headers={"Content-Type": "application/json"},
            json={
                "accessKey": access_key,
                "secret": secret,
                "tenantName": tenant_name,
            },
            timeout=30,
        )
        if response.status_code >= 400:
            raise ApiError("POST", auth_url, response.status_code, response.text)
        return json.loads(response.json()["jsonToken"])["access_token"]

    def _url(self, endpoint: str) -> str:
        """Monta a URL completa combinando a base com o endpoint.

        Args:
            endpoint (str): Caminho do endpoint, com ou sem barra inicial.

        Returns:
            str: URL completa no formato {base_url}/{endpoint}.
        """
        return f"{self._base_url}/{endpoint.lstrip('/')}"

    @staticmethod
    def _check(method: str, url: str, response: requests.Response) -> None:
        """Lança ApiError se a resposta contiver um código HTTP de erro.

        Args:
            method (str): Método HTTP utilizado.
            url (str): URL completa da requisição.
            response (requests.Response): Objeto de resposta retornado pela biblioteca requests.

        Raises:
            ApiError: Se o status HTTP for 400 ou superior.
        """
        if response.status_code >= 400:
            raise ApiError(method, url, response.status_code, response.text)

    def get(
        self, endpoint: str, params: Optional[dict[str, str]] = None
    ) -> dict[str, object] | list[object]:
        """Executa GET e retorna o corpo JSON da resposta.

        Args:
            endpoint (str): Caminho do endpoint.
            params (dict[str, str] | None): Parâmetros de query string opcionais.

        Returns:
            dict[str, object] | list[object]: Corpo da resposta deserializado como dict ou list.

        Raises:
            ApiError: Se a API retornar 4xx ou 5xx.
        """
        url = self._url(endpoint)
        response = self._session.get(url, params=params, timeout=30)
        self._check("GET", url, response)
        return cast(dict[str, object] | list[object], response.json())

    def post(
        self, endpoint: str, payload: dict[str, object]
    ) -> dict[str, object] | list[object]:
        """Executa POST e retorna o corpo JSON da resposta.

        Args:
            endpoint (str): Caminho do endpoint.
            payload (dict[str, object]): Corpo da requisição como dicionário.

        Returns:
            dict[str, object] | list[object]: Corpo da resposta deserializado.

        Raises:
            ApiError: Se a API retornar 4xx ou 5xx.
        """
        url = self._url(endpoint)
        response = self._session.post(url, json=payload, timeout=30)
        self._check("POST", url, response)
        return cast(dict[str, object] | list[object], response.json())

    def patch(
        self, endpoint: str, payload: dict[str, object]
    ) -> dict[str, object] | list[object]:
        """Executa PATCH e retorna o corpo JSON da resposta.

        Args:
            endpoint (str): Caminho do endpoint.
            payload (dict[str, object]): Corpo da requisição como dicionário.

        Returns:
            dict[str, object] | list[object]: Corpo da resposta deserializado.

        Raises:
            ApiError: Se a API retornar 4xx ou 5xx.
        """
        url = self._url(endpoint)
        response = self._session.patch(url, json=payload, timeout=30)
        self._check("PATCH", url, response)
        return cast(dict[str, object] | list[object], response.json())


class ApiSync(object):
    """Orquestra a sincronização de metadados YAML com a API DPSI Control Flex.

    Carrega as configurações do ambiente, autentica na API e executa o upsert
    de produtos, tabelas e master datas a partir dos arquivos YAML em metadata/.
    """

    def __init__(self, metadata_dir: Optional[Path] = None) -> None:
        """Inicializa ApiSync carregando as configurações e autenticando na API.

        Args:
            metadata_dir (Path | None): Caminho para a pasta de metadados. Quando None, usa
                a pasta metadata/ no mesmo diretório do script.

        Raises:
            EnvironmentError: Se alguma variável de ambiente obrigatória estiver ausente.
            ApiError: Se a autenticação na API falhar.
        """
        environment = get_environment()
        settings = self._load_settings(environment)
        self._client = self._build_client(settings)
        self._metadata_dir = metadata_dir or Path.cwd() / METADATA_DIR

    @staticmethod
    def _build_from_yaml(
        cls: type[_T], raw: dict[str, object], folder_name: str = ""
    ) -> _T:
        """Instancia *cls* lendo cada chave do YAML pelo nome do campo declarado.

        Regras por campo (em ordem de prioridade):
        1. metadata["folder_default"] = True → usa folder_name.upper() como fallback.
        2. Campo com default ou default_factory → usa o default como fallback.
        3. Campo obrigatório (sem default) → lê diretamente de raw (KeyError se ausente).

        Args:
            cls (type[_T]): Classe de destino a ser instanciada.
            raw (dict[str, object]): Dicionário com os dados lidos do YAML.
            folder_name (str): Nome da pasta do produto, usado como fallback para campos com folder_default.

        Returns:
            _T: Instância de cls preenchida com os dados do YAML.
        """
        kwargs: dict[str, Any] = {}
        for f in dataclasses.fields(cls):
            if f.metadata.get("folder_default"):
                kwargs[f.name] = raw.get(f.name, folder_name.upper())
            elif f.default is not dataclasses.MISSING:
                kwargs[f.name] = raw.get(f.name, f.default)
            elif f.default_factory is not dataclasses.MISSING:
                kwargs[f.name] = raw.get(f.name, f.default_factory())
            else:
                kwargs[f.name] = raw[f.name]
        return cls(**kwargs)

    @staticmethod
    def _to_api_payload(obj: Any) -> dict[str, Any]:
        """Serializa *obj* para dict da API, renomeando campos via metadata["api_name"].

        Args:
            obj (Any): Instância de dataclass a ser serializada.

        Returns:
            dict[str, Any]: Dicionário com os campos renomeados conforme o contrato da API.
        """
        payload: dict[str, Any] = {}
        for f in dataclasses.fields(obj):
            api_name = f.metadata.get("api_name", f.name)
            value = getattr(obj, f.name)
            payload[api_name] = (
                [
                    (
                        ApiSync._to_api_payload(item)
                        if dataclasses.is_dataclass(item)
                        else item
                    )
                    for item in value
                ]
                if isinstance(value, list)
                else value
            )
        return payload

    @staticmethod
    def _load_settings(environment: str) -> dict[str, str]:
        """Lê e valida as variáveis de ambiente obrigatórias.

        Utiliza a variável ENV para prefixar as demais chaves.

        Args:
            environment (str): Valor do ambiente (ex: "hml", "prod").

        Returns:
            dict[str, str]: Dicionário com as configurações carregadas.

        Raises:
            EnvironmentError: Se environment estiver vazio ou qualquer chave prefixada estiver ausente.
        """
        env = environment.upper()
        prefix = f"SOPS_DATACORE_{env}_DPSI_CONTROL_"
        keys = ["API_URL", "AUTH_URL", "ACCESS_KEY", "SECRET", "TENANT_NAME"]
        settings: dict[str, str] = {"env": env}
        missing = [] if env else ["ENV"]
        for key in keys:
            value = os.getenv(f"{prefix}{key}")
            settings[key.lower()] = value
            if not value:
                missing.append(key)
        if missing:
            print_message(
                f"Variáveis de ambiente obrigatórias não definidas: {', '.join(missing)}."
                " Entre em contato com o time de dados para verificar este problema.",
                Fore.RED,
            )
            sys.exit(1)
        return settings

    @staticmethod
    def _build_client(settings: dict[str, str]) -> _HttpClient:
        """Instancia e autentica o cliente HTTP a partir das configurações.

        Args:
            settings (dict[str, str]): Dicionário retornado por _load_settings.

        Returns:
            _HttpClient: Instância de _HttpClient autenticada e pronta para uso.
        """
        return _HttpClient(
            base_url=settings["api_url"],
            auth_url=settings["auth_url"],
            access_key=settings["access_key"],
            secret=settings["secret"],
            tenant_name=settings["tenant_name"],
        )

    @staticmethod
    def _read_product_yaml(path: Path, folder_name: str) -> Optional[_ProductData]:
        """Lê e deserializa o arquivo product.yaml de um produto.

        Args:
            path (Path): Caminho para o arquivo product.yaml.
            folder_name (str): Nome da pasta do produto, usado como fallback para o campo name.

        Returns:
            _ProductData | None: Instância de _ProductData, ou None se o arquivo não existir.
        """
        data = YamlHelper.load_yaml(path)
        return _ProductData.from_yaml(data["product"], folder_name) if data else None

    @staticmethod
    def _read_table_yaml(path: Path, folder_name: str) -> Optional[list[_TableData]]:
        """Lê e deserializa o arquivo tables.yaml de um produto.

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

        Returns:
            list[_TableData] | None: Lista de _TableData, ou None se o arquivo não existir.
        """
        data = YamlHelper.load_yaml(path)
        return (
            [_TableData.from_yaml(item, folder_name) for item in data.get("tables", [])]
            if data is not None
            else None
        )

    @staticmethod
    def _read_master_data_yaml(
        path: Path, folder_name: str
    ) -> Optional[list[_MasterDataData]]:
        """Lê e deserializa o arquivo master_datas.yaml de um produto.

        Args:
            path (Path): Caminho para o arquivo master_datas.yaml.
            folder_name (str): Nome da pasta do produto, usado como fallback para o campo product.

        Returns:
            list[_MasterDataData] | None: Lista de _MasterDataData, ou None se o arquivo não existir.
        """
        data = YamlHelper.load_yaml(path)
        return (
            [
                _MasterDataData.from_yaml(item, folder_name)
                for item in data.get("master_datas", [])
            ]
            if data is not None
            else None
        )

    def _upsert_product(self, data: _ProductData) -> tuple[int, int]:
        """Realiza o upsert de um produto na API.

        Busca o produto pelo name. Se existir, atualiza via PATCH;
        caso contrário, cria via POST.

        Args:
            data (_ProductData): Dados do produto a ser criado ou atualizado.

        Returns:
            tuple[int, int]: Tupla (created, updated) com a contagem da operação realizada.

        Raises:
            ApiError: Se a API retornar 4xx ou 5xx.
        """
        endpoint = "/api/v1/products"
        response = self._client.get(
            endpoint, params={"filter": f"name == '{data.name}'"}
        )
        items = response.get("contents", []) if isinstance(response, dict) else response
        payload = data.to_api_payload()
        if items:
            self._client.patch(f"{endpoint}/{items[0]['id']}", payload)
            print_message(
                f"UPDATE product name={data.name} id={items[0]['id']}", Fore.GREEN
            )
            return 0, 1
        self._client.post(endpoint, payload)
        print_message(f"CREATE product name={data.name}", Fore.GREEN)
        return 1, 0

    def _upsert_tables(self, tables: list[_TableData]) -> tuple[int, int]:
        """Realiza o upsert de uma lista de tabelas na API.

        Para cada tabela, busca pela UK (name + product + schema).
        Se existir, atualiza via PATCH; caso contrário, cria via POST.

        Args:
            tables (list[_TableData]): Lista de _TableData a ser sincronizada.

        Returns:
            tuple[int, int]: Tupla (created, updated) com a contagem total de operações realizadas.

        Raises:
            ApiError: Se a API retornar 4xx ou 5xx.
        """
        endpoint = "/api/v1/tables"
        created = updated = 0
        for data in tables:
            filter_expr = f"name == '{data.name}' and product.name == '{data.product}' and schema == '{data.schema}'"
            response = self._client.get(endpoint, params={"filter": filter_expr})
            items = (
                response.get("contents", []) if isinstance(response, dict) else response
            )
            payload = data.to_api_payload()
            if items:
                self._client.patch(f"{endpoint}/{items[0]['id']}", payload)
                print_message(
                    f"UPDATE table name={data.name} product={data.product} schema={data.schema} id={items[0]['id']}",
                    Fore.GREEN,
                )
                updated += 1
            else:
                self._client.post(endpoint, payload)
                print_message(
                    f"CREATE table name={data.name} product={data.product} schema={data.schema}",
                    Fore.GREEN,
                )
                created += 1
        return created, updated

    def _upsert_master_datas(
        self, master_datas: list[_MasterDataData]
    ) -> tuple[int, int]:
        """Realiza o upsert de uma lista de master datas na API.

        Para cada master data, busca pela UK (product + dest_name).
        Se existir, atualiza via PATCH; caso contrário, cria via POST.

        Args:
            master_datas (list[_MasterDataData]): Lista de _MasterDataData a ser sincronizada.

        Returns:
            tuple[int, int]: Tupla (created, updated) com a contagem total de operações realizadas.

        Raises:
            ApiError: Se a API retornar 4xx ou 5xx.
        """
        endpoint = "/api/v1/master-datas"
        created = updated = 0
        for data in master_datas:
            filter_expr = (
                f"product.name == '{data.product}' and dest_name == '{data.dest_name}'"
            )
            response = self._client.get(endpoint, params={"filter": filter_expr})
            items = (
                response.get("contents", []) if isinstance(response, dict) else response
            )
            payload = data.to_api_payload()
            if items:
                self._client.patch(f"{endpoint}/{items[0]['id']}", payload)
                print_message(
                    f"UPDATE master_data dest_name={data.dest_name} product={data.product} id={items[0]['id']}",
                    Fore.GREEN,
                )
                updated += 1
            else:
                self._client.post(endpoint, payload)
                print_message(
                    f"CREATE master_data dest_name={data.dest_name} product={data.product}",
                    Fore.GREEN,
                )
                created += 1
        return created, updated

    @staticmethod
    def _apply(
        fn: Callable[[], tuple[int, int] | None], summary: dict[str, int]
    ) -> None:
        """Executa uma função de upsert e acumula os resultados no summary.

        Chama fn() e, se o retorno for uma tupla (created, updated),
        acumula os valores em summary. Erros de API são capturados e
        registrados sem propagar a exceção.

        Args:
            fn (Callable[[], tuple[int, int] | None]): Callable sem argumentos que executa um upsert e retorna
                opcionalmente uma tupla (int, int) com as contagens.
            summary (dict[str, int]): Dicionário de contadores {'created', 'updated', 'errors'}
                atualizado in-place.
        """
        try:
            result = fn()
            if isinstance(result, tuple) and len(result) == 2:
                summary["created"] += result[0]
                summary["updated"] += result[1]
        except ApiError as exc:
            print_message(f"Erro HTTP: {exc}", Fore.RED)
            summary["errors"] += 1

    def _process_folder(self, folder: Path, summary: dict[str, int]) -> None:
        """Processa uma subpasta de metadados (product.yaml, tables.yaml, master_datas.yaml).

        Args:
            folder (Path): Subpasta do produto a processar (contém product.yaml, tables.yaml, master_datas.yaml).
            summary (dict[str, int]): Dicionário de contadores, atualizado in-place.
        """
        try:
            product_data = self._read_product_yaml(folder / PRODUCT_FILE, folder.name)
            if product_data:
                self._apply(lambda: self._upsert_product(product_data), summary)

            tables_data = self._read_table_yaml(folder / TABLES_FILE, folder.name)
            if tables_data:
                self._apply(lambda: self._upsert_tables(tables_data), summary)

            master_datas = self._read_master_data_yaml(
                folder / MASTER_DATAS_FILE, folder.name
            )
            if master_datas:
                self._apply(lambda: self._upsert_master_datas(master_datas), summary)
        except ApiError as exc:
            print_message(
                f"Erro HTTP ao processar produto '{folder.name}': {exc}", Fore.RED
            )
            summary["errors"] += 1
        except Exception as exc:
            print_message(
                f"Erro inesperado ao processar produto '{folder.name}': {exc}", Fore.RED
            )
            summary["errors"] += 1

    def _run_upsert(self) -> dict[str, int]:
        """Percorre todas as subpastas de metadata_dir e executa os upserts.

        Ordem de execução por subpasta: product.yaml → tables.yaml → master_datas.yaml.
        Erros em uma pasta são capturados e logados sem interromper as demais.

        Returns:
            dict[str, int]: Dicionário com contagem de resultados: {'created': N, 'updated': N, 'errors': N}.
        """
        summary: dict[str, int] = {"created": 0, "updated": 0, "errors": 0}

        if not self._metadata_dir.exists() or not self._metadata_dir.is_dir():
            print_message(
                f"Diretório de metadados não encontrado ou não é uma pasta: {self._metadata_dir}",
                Fore.RED,
            )
            return summary

        folders = sorted(p for p in self._metadata_dir.iterdir() if p.is_dir())
        if not folders:
            print_message(f"Nenhuma subpasta encontrada em {self._metadata_dir}")
            return summary

        for folder in folders:
            print_message(f"=== Processando produto: {folder.name} ===")
            self._process_folder(folder, summary)

        return summary

    def main(self) -> dict[str, int]:
        """Ponto de entrada principal: executa o upsert completo e reporta o resultado.

        Loga o progresso, exibe o resumo final e retorna o dicionário de contagens.

        Returns:
            dict[str, int]: Dicionário com contagens {'created', 'updated', 'errors'}.
        """
        print_message(f"Iniciando upsert de metadados em: {self._metadata_dir}")
        summary = self._run_upsert()
        print_message(
            f"Upsert concluído — "
            f"criados: {summary['created']} | atualizados: {summary['updated']} | erros: {summary['errors']}"
        )
        if summary["errors"] > 0:
            print_message(
                "Alguns erros ocorreram durante o processo. Verifique os logs para detalhes.",
                Fore.RED,
            )
        return summary
