#!/usr/bin/env python3
# -*- coding: utf-8 -*-

"""
Analisador híbrido estável:
- MediaPipe Tasks Face Landmarker para localizar 478 pontos faciais.
- OpenCV/NumPy para medir características visuais por região.
- OpenCV Haar como fallback integral.
- Saída JSON única e limpa.

Não realiza diagnóstico médico e não mede clinicamente hidratação,
sebo, elasticidade ou colágeno.
"""

from __future__ import annotations

import os

os.environ.setdefault("TF_CPP_MIN_LOG_LEVEL", "3")
os.environ.setdefault("GLOG_minloglevel", "3")
os.environ.setdefault("MEDIAPIPE_DISABLE_GPU", "1")
os.environ.setdefault("PYTHONIOENCODING", "utf-8")
os.environ.setdefault("PYTHONUNBUFFERED", "1")

import json
import math
import sys
import traceback
from dataclasses import dataclass
from pathlib import Path
from typing import Any, Dict, List, Optional, Sequence, Tuple

import cv2
import numpy as np

try:
    cv2.setNumThreads(1)
except Exception:
    pass

try:
    import mediapipe as mp
    MEDIAPIPE_IMPORTADO = True
except Exception:
    mp = None
    MEDIAPIPE_IMPORTADO = False


VERSAO = "PELE_HIBRIDO_MEDIAPIPE_TASKS_OPENCV_4.0"
MODELO_PADRAO = Path(
    os.environ.get(
        "PELE_FACE_LANDMARKER_MODEL",
        "/opt/pele-ai/modelos/face_landmarker.task"
    )
)

MIN_LARGURA = 480
MIN_ALTURA = 480
MAX_DIMENSAO = 1800
ETAPA = "INICIALIZACAO"

# MediaPipe Face Landmarker indexes.
FACE_OVAL = [
    10, 338, 297, 332, 284, 251, 389, 356, 454, 323, 361, 288,
    397, 365, 379, 378, 400, 377, 152, 148, 176, 149, 150, 136,
    172, 58, 132, 93, 234, 127, 162, 21, 54, 103, 67, 109
]
LEFT_EYE = [33, 160, 158, 133, 153, 144]
RIGHT_EYE = [362, 385, 387, 263, 373, 380]
LIPS = [
    61, 185, 40, 39, 37, 0, 267, 269, 270, 409, 291,
    375, 321, 405, 314, 17, 84, 181, 91, 146
]
LEFT_IRIS = [468, 469, 470, 471, 472]
RIGHT_IRIS = [473, 474, 475, 476, 477]


@dataclass
class FaceData:
    bbox: Tuple[int, int, int, int]
    landmarks: Optional[np.ndarray]
    metodo: str
    inclinacao: float
    giro_horizontal: float
    cobertura: float
    confianca_localizacao: float
    avisos: List[str]


def limitar(valor: float) -> float:
    return float(max(0.0, min(100.0, valor)))


def nivel_menor_melhor(valor: float) -> str:
    if valor < 34:
        return "BAIXO"
    if valor < 67:
        return "MODERADO"
    return "ELEVADO"


def nivel_maior_melhor(valor: float) -> str:
    if valor >= 67:
        return "ADEQUADO"
    if valor >= 40:
        return "MODERADO"
    return "BAIXO"


def emitir_json(
    sucesso: bool,
    mensagem: str,
    codigo: int = 0,
    dados: Optional[Dict[str, Any]] = None
) -> None:
    resposta: Dict[str, Any] = {
        "sucesso": sucesso,
        "mensagem": mensagem,
        "motor": VERSAO,
        "etapa": ETAPA
    }

    if dados:
        resposta.update(dados)

    print(
        json.dumps(
            resposta,
            ensure_ascii=False,
            allow_nan=False
        ),
        flush=True
    )

    raise SystemExit(codigo)


def metrica(
    valor: Optional[float],
    nivel: str,
    descricao: str,
    confianca: float,
    limitacao: Optional[str] = None,
    componentes: Optional[Dict[str, Any]] = None
) -> Dict[str, Any]:
    item: Dict[str, Any] = {
        "nivel": nivel,
        "valor": (
            None
            if valor is None
            else round(limitar(valor), 2)
        ),
        "descricao": descricao,
        "confianca": round(limitar(confianca), 2)
    }

    if limitacao:
        item["limitacao"] = limitacao

    if componentes:
        item["componentes"] = componentes

    return item


def carregar_imagem(caminho: Path) -> np.ndarray:
    global ETAPA
    ETAPA = "CARREGAR_IMAGEM"

    if not caminho.is_file():
        emitir_json(
            False,
            "A fotografia não foi encontrada no servidor.",
            2
        )

    bruto = np.fromfile(
        str(caminho),
        dtype=np.uint8
    )

    imagem = cv2.imdecode(
        bruto,
        cv2.IMREAD_COLOR
    )

    if imagem is None or imagem.size == 0:
        emitir_json(
            False,
            "A fotografia enviada é inválida ou não pôde ser aberta.",
            2
        )

    altura, largura = imagem.shape[:2]

    if largura < MIN_LARGURA or altura < MIN_ALTURA:
        emitir_json(
            False,
            "A fotografia possui resolução baixa. Tire outra foto com mais qualidade.",
            2,
            {
                "largura": largura,
                "altura": altura
            }
        )

    maior = max(largura, altura)

    if maior > MAX_DIMENSAO:
        escala = MAX_DIMENSAO / float(maior)
        imagem = cv2.resize(
            imagem,
            (
                int(round(largura * escala)),
                int(round(altura * escala))
            ),
            interpolation=cv2.INTER_AREA
        )

    return imagem


def detectar_mediapipe_tasks(
    imagem_bgr: np.ndarray
) -> Optional[FaceData]:
    global ETAPA
    ETAPA = "MEDIAPIPE_FACE_LANDMARKER"

    if not MEDIAPIPE_IMPORTADO:
        return None

    if not MODELO_PADRAO.is_file():
        return None

    try:
        BaseOptions = mp.tasks.BaseOptions
        FaceLandmarker = mp.tasks.vision.FaceLandmarker
        FaceLandmarkerOptions = (
            mp.tasks.vision.FaceLandmarkerOptions
        )
        RunningMode = mp.tasks.vision.RunningMode
    except Exception:
        return None

    altura, largura = imagem_bgr.shape[:2]

    try:
        imagem_rgb = cv2.cvtColor(
            imagem_bgr,
            cv2.COLOR_BGR2RGB
        )

        imagem_mp = mp.Image(
            image_format=mp.ImageFormat.SRGB,
            data=np.ascontiguousarray(imagem_rgb)
        )

        opcoes = FaceLandmarkerOptions(
            base_options=BaseOptions(
                model_asset_path=str(MODELO_PADRAO)
            ),
            running_mode=RunningMode.IMAGE,
            num_faces=2,
            min_face_detection_confidence=0.55,
            min_face_presence_confidence=0.55,
            min_tracking_confidence=0.50,
            output_face_blendshapes=False,
            output_facial_transformation_matrixes=True
        )

        with FaceLandmarker.create_from_options(
            opcoes
        ) as landmarker:
            resultado = landmarker.detect(
                imagem_mp
            )

        rostos = getattr(
            resultado,
            "face_landmarks",
            None
        )

        if not rostos:
            return None

        if len(rostos) != 1:
            emitir_json(
                False,
                "A fotografia precisa conter exatamente um rosto.",
                2,
                {
                    "rostos_detectados": len(rostos)
                }
            )

        pontos = rostos[0]

        if len(pontos) < 468:
            return None

        landmarks = np.array(
            [
                [
                    float(p.x) * largura,
                    float(p.y) * altura,
                    float(p.z) * largura
                ]
                for p in pontos
            ],
            dtype=np.float32
        )

        if (
            landmarks.ndim != 2 or
            landmarks.shape[1] < 2 or
            not np.all(
                np.isfinite(
                    landmarks[:, :2]
                )
            )
        ):
            return None

        x1 = max(
            0,
            int(np.floor(
                np.min(
                    landmarks[:, 0]
                )
            ))
        )
        y1 = max(
            0,
            int(np.floor(
                np.min(
                    landmarks[:, 1]
                )
            ))
        )
        x2 = min(
            largura - 1,
            int(np.ceil(
                np.max(
                    landmarks[:, 0]
                )
            ))
        )
        y2 = min(
            altura - 1,
            int(np.ceil(
                np.max(
                    landmarks[:, 1]
                )
            ))
        )

        if x2 <= x1 or y2 <= y1:
            return None

        olho_esquerdo = np.mean(
            landmarks[LEFT_EYE, :2],
            axis=0
        )
        olho_direito = np.mean(
            landmarks[RIGHT_EYE, :2],
            axis=0
        )

        vetor_olhos = (
            olho_direito -
            olho_esquerdo
        )

        inclinacao = math.degrees(
            math.atan2(
                float(vetor_olhos[1]),
                float(vetor_olhos[0])
            )
        )

        nariz_x = float(
            landmarks[1, 0]
        )

        centro_x = (
            x1 + x2
        ) / 2.0

        giro = (
            (nariz_x - centro_x) /
            max(
                1.0,
                (x2 - x1) / 2.0
            ) *
            45.0
        )

        cobertura = (
            (x2 - x1) *
            (y2 - y1)
        ) / float(
            largura * altura
        )

        avisos: List[str] = []

        # Óculos/reflexo: heurística conservadora.
        if len(pontos) >= 478:
            regiao_olhos = landmarks[
                LEFT_EYE +
                RIGHT_EYE +
                LEFT_IRIS +
                RIGHT_IRIS,
                :2
            ]

            min_x = max(
                0,
                int(
                    np.min(
                        regiao_olhos[:, 0]
                    )
                )
            )
            max_x = min(
                largura,
                int(
                    np.max(
                        regiao_olhos[:, 0]
                    )
                )
            )
            min_y = max(
                0,
                int(
                    np.min(
                        regiao_olhos[:, 1]
                    )
                ) -
                int(
                    (y2 - y1) * 0.05
                )
            )
            max_y = min(
                altura,
                int(
                    np.max(
                        regiao_olhos[:, 1]
                    )
                ) +
                int(
                    (y2 - y1) * 0.05
                )
            )

            recorte_olhos = imagem_bgr[
                min_y:max_y,
                min_x:max_x
            ]

            if recorte_olhos.size > 0:
                cinza_olhos = cv2.cvtColor(
                    recorte_olhos,
                    cv2.COLOR_BGR2GRAY
                )

                reflexos = float(
                    np.mean(
                        cinza_olhos >= 240
                    ) * 100.0
                )

                bordas = float(
                    np.mean(
                        cv2.Canny(
                            cinza_olhos,
                            70,
                            150
                        ) > 0
                    ) * 100.0
                )

                if reflexos > 2.5 or bordas > 18:
                    avisos.append(
                        "Possível presença de óculos/reflexo; "
                        "a confiança da região dos olhos foi reduzida."
                    )

        return FaceData(
            bbox=(x1, y1, x2, y2),
            landmarks=landmarks,
            metodo="MEDIAPIPE_TASKS_FACE_LANDMARKER",
            inclinacao=inclinacao,
            giro_horizontal=giro,
            cobertura=cobertura,
            confianca_localizacao=94.0,
            avisos=avisos
        )

    except SystemExit:
        raise
    except Exception:
        return None


def detectar_opencv(
    imagem: np.ndarray
) -> FaceData:
    global ETAPA
    ETAPA = "OPENCV_HAAR_FALLBACK"

    cinza = cv2.cvtColor(
        imagem,
        cv2.COLOR_BGR2GRAY
    )

    equalizada = cv2.equalizeHist(
        cinza
    )

    detectores = [
        (
            "haarcascade_frontalface_alt2",
            cv2.data.haarcascades +
            "haarcascade_frontalface_alt2.xml"
        ),
        (
            "haarcascade_frontalface_default",
            cv2.data.haarcascades +
            "haarcascade_frontalface_default.xml"
        )
    ]

    candidatos: List[
        Tuple[int, int, int, int, str]
    ] = []

    for nome, caminho in detectores:
        cascade = cv2.CascadeClassifier(
            caminho
        )

        if cascade.empty():
            continue

        rostos = cascade.detectMultiScale(
            equalizada,
            scaleFactor=1.055,
            minNeighbors=5,
            minSize=(130, 130),
            flags=cv2.CASCADE_SCALE_IMAGE
        )

        for x, y, w, h in rostos:
            candidatos.append(
                (
                    int(x),
                    int(y),
                    int(w),
                    int(h),
                    nome
                )
            )

    if not candidatos:
        emitir_json(
            False,
            "Nenhum rosto frontal foi localizado. "
            "Tire outra foto olhando diretamente para a câmera, "
            "com o rosto maior no quadro e boa iluminação.",
            2,
            {
                "mediapipe_disponivel":
                    MEDIAPIPE_IMPORTADO,
                "modelo_mediapipe":
                    str(MODELO_PADRAO),
                "modelo_existe":
                    MODELO_PADRAO.is_file()
            }
        )

    candidatos.sort(
        key=lambda item:
            item[2] * item[3],
        reverse=True
    )

    x, y, w, h, detector = (
        candidatos[0]
    )

    altura_img, largura_img = (
        imagem.shape[:2]
    )

    margem_x = int(w * 0.06)
    margem_y = int(h * 0.07)

    x1 = max(
        0,
        x - margem_x
    )
    y1 = max(
        0,
        y - margem_y
    )
    x2 = min(
        largura_img - 1,
        x + w + margem_x
    )
    y2 = min(
        altura_img - 1,
        y + h + margem_y
    )

    cobertura = (
        (x2 - x1) *
        (y2 - y1)
    ) / float(
        largura_img * altura_img
    )

    return FaceData(
        bbox=(x1, y1, x2, y2),
        landmarks=None,
        metodo=(
            "OPENCV_HAAR_FALLBACK_" +
            detector
        ),
        inclinacao=0.0,
        giro_horizontal=0.0,
        cobertura=cobertura,
        confianca_localizacao=58.0,
        avisos=[
            "MediaPipe não foi usado nesta foto; "
            "as regiões foram aproximadas pelo OpenCV."
        ]
    )


def detectar_rosto(
    imagem: np.ndarray
) -> FaceData:
    face = detectar_mediapipe_tasks(
        imagem
    )

    if face is not None:
        return face

    return detectar_opencv(
        imagem
    )


def mascara_poligono(
    forma: Tuple[int, int],
    pontos: np.ndarray
) -> np.ndarray:
    mascara = np.zeros(
        forma,
        dtype=np.uint8
    )

    pontos_int = np.round(
        pontos[:, :2]
    ).astype(
        np.int32
    )

    cv2.fillPoly(
        mascara,
        [pontos_int],
        255
    )

    return mascara


def mascara_elipse(
    forma: Tuple[int, int],
    centro: Tuple[float, float],
    eixos: Tuple[float, float]
) -> np.ndarray:
    mascara = np.zeros(
        forma,
        dtype=np.uint8
    )

    cv2.ellipse(
        mascara,
        (
            int(centro[0]),
            int(centro[1])
        ),
        (
            max(1, int(eixos[0])),
            max(1, int(eixos[1]))
        ),
        0,
        0,
        360,
        255,
        -1
    )

    return mascara


def area_mascara(
    mascara: np.ndarray
) -> int:
    return int(
        np.count_nonzero(
            mascara
        )
    )


def criar_mascaras(
    imagem: np.ndarray,
    face: FaceData
) -> Dict[str, np.ndarray]:
    global ETAPA
    ETAPA = "CRIAR_MASCARAS"

    altura, largura = imagem.shape[:2]
    forma = (altura, largura)
    x1, y1, x2, y2 = face.bbox
    fw = max(2, x2 - x1)
    fh = max(2, y2 - y1)
    cx = (x1 + x2) / 2.0
    cy = (y1 + y2) / 2.0

    face_mask: Optional[
        np.ndarray
    ] = None

    pele: Optional[
        np.ndarray
    ] = None

    landmarks_ok = (
        face.landmarks is not None and
        isinstance(
            face.landmarks,
            np.ndarray
        ) and
        face.landmarks.ndim == 2 and
        face.landmarks.shape[0] >
            max(
                max(FACE_OVAL),
                max(LEFT_EYE),
                max(RIGHT_EYE),
                max(LIPS)
            )
    )

    if landmarks_ok:
        try:
            landmarks = face.landmarks

            face_mask = mascara_poligono(
                forma,
                landmarks[FACE_OVAL]
            )

            olhos = cv2.bitwise_or(
                mascara_poligono(
                    forma,
                    landmarks[LEFT_EYE]
                ),
                mascara_poligono(
                    forma,
                    landmarks[RIGHT_EYE]
                )
            )

            labios = mascara_poligono(
                forma,
                landmarks[LIPS]
            )

            kernel_olhos = (
                cv2.getStructuringElement(
                    cv2.MORPH_ELLIPSE,
                    (
                        max(
                            9,
                            int(fw * 0.055)
                        ) | 1,
                        max(
                            9,
                            int(fh * 0.035)
                        ) | 1
                    )
                )
            )

            olhos = cv2.dilate(
                olhos,
                kernel_olhos
            )

            kernel_labios = (
                cv2.getStructuringElement(
                    cv2.MORPH_ELLIPSE,
                    (
                        max(
                            7,
                            int(fw * 0.035)
                        ) | 1,
                        max(
                            7,
                            int(fh * 0.025)
                        ) | 1
                    )
                )
            )

            labios = cv2.dilate(
                labios,
                kernel_labios
            )

            pele = cv2.bitwise_and(
                face_mask,
                cv2.bitwise_not(
                    olhos
                )
            )

            pele = cv2.bitwise_and(
                pele,
                cv2.bitwise_not(
                    labios
                )
            )

            if area_mascara(
                pele
            ) < 2500:
                face_mask = None
                pele = None

        except Exception:
            face_mask = None
            pele = None

    if face_mask is None or pele is None:
        face_mask = mascara_elipse(
            forma,
            (cx, cy),
            (
                fw * 0.46,
                fh * 0.49
            )
        )

        olhos = cv2.bitwise_or(
            mascara_elipse(
                forma,
                (
                    cx - fw * 0.18,
                    y1 + fh * 0.40
                ),
                (
                    fw * 0.14,
                    fh * 0.075
                )
            ),
            mascara_elipse(
                forma,
                (
                    cx + fw * 0.18,
                    y1 + fh * 0.40
                ),
                (
                    fw * 0.14,
                    fh * 0.075
                )
            )
        )

        boca = mascara_elipse(
            forma,
            (
                cx,
                y1 + fh * 0.73
            ),
            (
                fw * 0.19,
                fh * 0.075
            )
        )

        exclusoes = cv2.bitwise_or(
            olhos,
            boca
        )

        pele = cv2.bitwise_and(
            face_mask,
            cv2.bitwise_not(
                exclusoes
            )
        )

    testa = np.zeros(
        forma,
        dtype=np.uint8
    )

    cv2.rectangle(
        testa,
        (
            max(
                0,
                int(cx - fw * 0.23)
            ),
            max(
                0,
                int(y1 + fh * 0.13)
            )
        ),
        (
            min(
                largura - 1,
                int(cx + fw * 0.23)
            ),
            min(
                altura - 1,
                int(y1 + fh * 0.32)
            )
        ),
        255,
        -1
    )

    nariz = mascara_elipse(
        forma,
        (
            cx,
            y1 + fh * 0.52
        ),
        (
            fw * 0.105,
            fh * 0.22
        )
    )

    queixo = mascara_elipse(
        forma,
        (
            cx,
            y1 + fh * 0.82
        ),
        (
            fw * 0.17,
            fh * 0.09
        )
    )

    bochecha_e = mascara_elipse(
        forma,
        (
            cx - fw * 0.23,
            y1 + fh * 0.58
        ),
        (
            fw * 0.15,
            fh * 0.13
        )
    )

    bochecha_d = mascara_elipse(
        forma,
        (
            cx + fw * 0.23,
            y1 + fh * 0.58
        ),
        (
            fw * 0.15,
            fh * 0.13
        )
    )

    sob_olhos = cv2.bitwise_or(
        mascara_elipse(
            forma,
            (
                cx - fw * 0.18,
                y1 + fh * 0.47
            ),
            (
                fw * 0.13,
                fh * 0.05
            )
        ),
        mascara_elipse(
            forma,
            (
                cx + fw * 0.18,
                y1 + fh * 0.47
            ),
            (
                fw * 0.13,
                fh * 0.05
            )
        )
    )

    zona_t = cv2.bitwise_or(
        testa,
        nariz
    )

    zona_t = cv2.bitwise_or(
        zona_t,
        queixo
    )

    bochechas = cv2.bitwise_or(
        bochecha_e,
        bochecha_d
    )

    return {
        "face": face_mask,
        "pele": pele,
        "testa":
            cv2.bitwise_and(
                testa,
                pele
            ),
        "nariz":
            cv2.bitwise_and(
                nariz,
                pele
            ),
        "queixo":
            cv2.bitwise_and(
                queixo,
                pele
            ),
        "zona_t":
            cv2.bitwise_and(
                zona_t,
                pele
            ),
        "bochecha_esquerda":
            cv2.bitwise_and(
                bochecha_e,
                pele
            ),
        "bochecha_direita":
            cv2.bitwise_and(
                bochecha_d,
                pele
            ),
        "bochechas":
            cv2.bitwise_and(
                bochechas,
                pele
            ),
        "olhos_inferiores":
            cv2.bitwise_and(
                sob_olhos,
                face_mask
            )
    }


def valores(
    canal: np.ndarray,
    mascara: np.ndarray
) -> np.ndarray:
    amostra = canal[
        mascara > 0
    ]

    if amostra.size == 0:
        return np.array(
            [],
            dtype=np.float32
        )

    return amostra.astype(
        np.float32
    )


def validar_qualidade(
    imagem: np.ndarray,
    face: FaceData,
    mascaras: Dict[str, np.ndarray]
) -> Tuple[
    str,
    float,
    List[str],
    Dict[str, float]
]:
    global ETAPA
    ETAPA = "VALIDAR_QUALIDADE"

    cinza = cv2.cvtColor(
        imagem,
        cv2.COLOR_BGR2GRAY
    )

    pele = valores(
        cinza,
        mascaras["pele"]
    )

    if pele.size < 2500:
        emitir_json(
            False,
            "Pouca área de pele ficou visível. "
            "Retire objetos que cubram o rosto e tire outra foto.",
            2
        )

    nitidez = float(
        cv2.Laplacian(
            cinza,
            cv2.CV_64F
        ).var()
    )

    luminosidade = float(
        np.mean(pele)
    )

    contraste = float(
        np.std(pele)
    )

    estourados = float(
        np.mean(
            pele >= 248
        ) * 100.0
    )

    sombras = float(
        np.mean(
            pele <= 8
        ) * 100.0
    )

    lado_e = valores(
        cinza,
        mascaras[
            "bochecha_esquerda"
        ]
    )

    lado_d = valores(
        cinza,
        mascaras[
            "bochecha_direita"
        ]
    )

    diferenca_lados = 0.0

    if (
        lado_e.size > 0 and
        lado_d.size > 0
    ):
        diferenca_lados = abs(
            float(np.median(lado_e)) -
            float(np.median(lado_d))
        )

    falhas: List[str] = []
    avisos = list(face.avisos)

    if nitidez < 34:
        falhas.append(
            "A foto está muito desfocada."
        )
    elif nitidez < 65:
        avisos.append(
            "A nitidez está apenas regular."
        )

    if luminosidade < 46:
        falhas.append(
            "O rosto está muito escuro."
        )
    elif luminosidade < 65:
        avisos.append(
            "A iluminação está um pouco baixa."
        )

    if luminosidade > 228:
        falhas.append(
            "O rosto está superexposto."
        )
    elif luminosidade > 210:
        avisos.append(
            "A iluminação está um pouco forte."
        )

    if contraste < 13:
        falhas.append(
            "A imagem tem contraste insuficiente."
        )

    if estourados > 9:
        falhas.append(
            "Há reflexos estourados em excesso."
        )

    if sombras > 11:
        falhas.append(
            "Há sombras profundas em excesso."
        )

    if diferenca_lados > 40:
        falhas.append(
            "A iluminação está muito desigual entre os lados."
        )
    elif diferenca_lados > 23:
        avisos.append(
            "Existe diferença de iluminação entre os lados."
        )

    if face.cobertura < 0.10:
        falhas.append(
            "O rosto está muito distante."
        )
    elif face.cobertura < 0.16:
        avisos.append(
            "Aproxime um pouco mais o rosto."
        )

    if face.cobertura > 0.84:
        falhas.append(
            "O rosto está muito próximo ou cortado."
        )

    if abs(face.inclinacao) > 14:
        falhas.append(
            "Mantenha a cabeça mais reta."
        )
    elif abs(face.inclinacao) > 8:
        avisos.append(
            "A cabeça está levemente inclinada."
        )

    if abs(
        face.giro_horizontal
    ) > 17:
        falhas.append(
            "Olhe diretamente para a câmera."
        )
    elif abs(
        face.giro_horizontal
    ) > 10:
        avisos.append(
            "O rosto está levemente girado."
        )

    if falhas:
        emitir_json(
            False,
            "A fotografia não atende aos critérios mínimos. " +
            " ".join(falhas),
            2,
            {
                "falhas": falhas,
                "avisos": avisos,
                "detector_facial":
                    face.metodo
            }
        )

    pontuacao = 100.0
    pontuacao -= min(
        25.0,
        max(
            0.0,
            70.0 - nitidez
        ) * 0.43
    )
    pontuacao -= min(
        18.0,
        abs(
            luminosidade - 135.0
        ) * 0.14
    )
    pontuacao -= min(
        14.0,
        diferenca_lados * 0.33
    )
    pontuacao -= min(
        10.0,
        abs(face.inclinacao) * 0.45
    )
    pontuacao -= min(
        10.0,
        abs(
            face.giro_horizontal
        ) * 0.45
    )

    if face.metodo.startswith(
        "OPENCV_HAAR"
    ):
        pontuacao -= 8.0

    pontuacao = limitar(
        pontuacao
    )

    qualidade = (
        "BOA"
        if pontuacao >= 72
        else "REGULAR"
    )

    return (
        qualidade,
        pontuacao,
        avisos,
        {
            "nitidez":
                round(nitidez, 2),
            "luminosidade":
                round(
                    luminosidade,
                    2
                ),
            "contraste":
                round(
                    contraste,
                    2
                ),
            "reflexos_percentual":
                round(
                    estourados,
                    2
                ),
            "sombras_percentual":
                round(
                    sombras,
                    2
                ),
            "diferenca_lados":
                round(
                    diferenca_lados,
                    2
                ),
            "inclinacao_graus":
                round(
                    abs(
                        face.inclinacao
                    ),
                    2
                ),
            "giro_estimado_graus":
                round(
                    abs(
                        face.giro_horizontal
                    ),
                    2
                ),
            "cobertura_rosto":
                round(
                    face.cobertura,
                    5
                ),
            "confianca_localizacao":
                round(
                    face.confianca_localizacao,
                    2
                )
        }
    )


def executar_seguro(
    nome: str,
    funcao
) -> Dict[str, Any]:
    global ETAPA
    ETAPA = (
        "METRICA_" +
        nome.upper()
    )

    try:
        resultado = funcao()

        if not isinstance(
            resultado,
            dict
        ):
            raise ValueError(
                "resultado interno inválido"
            )

        return resultado

    except Exception as erro:
        return metrica(
            None,
            "NAO_CONCLUSIVO",
            f"{nome} não pôde ser concluída nesta fotografia.",
            0.0,
            (
                "Falha isolada; as outras métricas continuam. "
                f"Detalhe técnico: {str(erro)[:140]}"
            )
        )


def analisar_metricas(
    imagem: np.ndarray,
    mascaras: Dict[str, np.ndarray],
    qualidade: float,
    face: FaceData
) -> Dict[str, Dict[str, Any]]:
    cinza = cv2.cvtColor(
        imagem,
        cv2.COLOR_BGR2GRAY
    )

    hsv = cv2.cvtColor(
        imagem,
        cv2.COLOR_BGR2HSV
    )

    lab = cv2.cvtColor(
        imagem,
        cv2.COLOR_BGR2LAB
    )

    def oleosidade() -> Dict[str, Any]:
        v = valores(
            hsv[:, :, 2],
            mascaras["zona_t"]
        )
        s = valores(
            hsv[:, :, 1],
            mascaras["zona_t"]
        )

        if v.size < 300:
            raise ValueError(
                "Zona T insuficiente"
            )

        p75 = float(
            np.percentile(
                v,
                75
            )
        )
        p95 = float(
            np.percentile(
                v,
                95
            )
        )
        med_s = float(
            np.median(s)
        )

        realces = float(
            np.mean(
                (v >= p95) &
                (
                    s <=
                    max(
                        60.0,
                        med_s
                    )
                )
            ) * 100.0
        )

        valor = limitar(
            realces * 6.2 +
            max(
                0.0,
                p95 - p75
            ) * 1.4
        )

        return metrica(
            valor,
            nivel_menor_melhor(
                valor
            ),
            (
                "Brilho superficial aparente "
                f"da Zona T estimado em {valor:.0f}/100."
            ),
            qualidade * 0.88,
            (
                "Estimativa visual; "
                "não mede sebo com equipamento."
            ),
            {
                "realces_percentual":
                    round(
                        realces,
                        2
                    ),
                "intervalo_p95_p75":
                    round(
                        p95 - p75,
                        2
                    )
            }
        )

    def poros() -> Dict[str, Any]:
        mascara = cv2.bitwise_or(
            mascaras["bochechas"],
            mascaras["nariz"]
        )

        suavizada = cv2.GaussianBlur(
            cinza,
            (0, 0),
            3.0
        )

        resposta = cv2.subtract(
            suavizada,
            cinza
        )

        amostra = valores(
            resposta,
            mascara
        )

        if amostra.size < 500:
            raise ValueError(
                "Área de poros insuficiente"
            )

        limite = max(
            6.0,
            float(
                np.percentile(
                    amostra,
                    82
                )
            )
        )

        proporcao = float(
            np.mean(
                amostra >= limite
            ) * 100.0
        )

        intensidade = float(
            np.percentile(
                amostra,
                92
            )
        )

        valor = limitar(
            proporcao * 7.0 +
            intensidade * 1.5
        )

        return metrica(
            valor,
            nivel_menor_melhor(
                valor
            ),
            (
                "Visibilidade aparente de poros "
                f"e textura estimada em {valor:.0f}/100."
            ),
            qualidade * 0.84
        )

    def linhas() -> Dict[str, Any]:
        mascara = cv2.bitwise_or(
            mascaras["testa"],
            mascaras[
                "olhos_inferiores"
            ]
        )

        clahe = cv2.createCLAHE(
            clipLimit=1.5,
            tileGridSize=(8, 8)
        ).apply(cinza)

        kernel = cv2.getStructuringElement(
            cv2.MORPH_ELLIPSE,
            (13, 5)
        )

        blackhat = cv2.morphologyEx(
            clahe,
            cv2.MORPH_BLACKHAT,
            kernel
        )

        amostra = valores(
            blackhat,
            mascara
        )

        if amostra.size < 350:
            raise ValueError(
                "Área de linhas insuficiente"
            )

        limite = max(
            12.0,
            float(
                np.percentile(
                    amostra,
                    84
                )
            )
        )

        proporcao = float(
            np.mean(
                amostra >= limite
            ) * 100.0
        )

        intensidade = float(
            np.percentile(
                amostra,
                94
            )
        )

        valor = limitar(
            proporcao * 7.4 +
            intensidade * 1.1
        )

        confianca = qualidade * 0.82

        if any(
            "óculos" in aviso.lower()
            for aviso in face.avisos
        ):
            confianca *= 0.72

        return metrica(
            valor,
            nivel_menor_melhor(
                valor
            ),
            (
                "Linhas superficiais aparentes "
                f"estimadas em {valor:.0f}/100."
            ),
            confianca,
            (
                "A confiança da região dos olhos "
                "é reduzida quando há óculos ou reflexos."
            )
        )

    def tonalidade() -> Dict[str, Any]:
        regioes = [
            mascaras["testa"],
            mascaras[
                "bochecha_esquerda"
            ],
            mascaras[
                "bochecha_direita"
            ],
            mascaras["queixo"]
        ]

        medianas = []

        for mascara in regioes:
            l = valores(
                lab[:, :, 0],
                mascara
            )
            a = valores(
                lab[:, :, 1],
                mascara
            )
            b = valores(
                lab[:, :, 2],
                mascara
            )

            if l.size > 120:
                medianas.append(
                    [
                        float(
                            np.median(l)
                        ),
                        float(
                            np.median(a)
                        ),
                        float(
                            np.median(b)
                        )
                    ]
                )

        if len(medianas) < 3:
            raise ValueError(
                "Regiões insuficientes"
            )

        variacao = float(
            np.mean(
                np.std(
                    np.array(
                        medianas,
                        dtype=np.float32
                    ),
                    axis=0
                )
            )
        )

        valor = limitar(
            variacao * 5.0
        )

        return metrica(
            valor,
            nivel_menor_melhor(
                valor
            ),
            (
                "Irregularidade visual de tonalidade "
                f"estimada em {valor:.0f}/100."
            ),
            qualidade * 0.86
        )

    def vermelhidao() -> Dict[str, Any]:
        mascara = cv2.bitwise_or(
            mascaras["bochechas"],
            mascaras["testa"]
        )

        canal_a = valores(
            lab[:, :, 1],
            mascara
        )

        if canal_a.size < 400:
            raise ValueError(
                "Área insuficiente"
            )

        mediana = float(
            np.median(
                canal_a
            )
        )

        p85 = float(
            np.percentile(
                canal_a,
                85
            )
        )

        valor = limitar(
            (
                mediana -
                126.0
            ) * 4.0 +
            max(
                0.0,
                p85 - mediana
            ) * 2.0
        )

        return metrica(
            valor,
            nivel_menor_melhor(
                valor
            ),
            (
                "Vermelhidão visual aparente "
                f"estimada em {valor:.0f}/100."
            ),
            qualidade * 0.85
        )

    resultado = {
        "oleosidade":
            executar_seguro(
                "oleosidade",
                oleosidade
            ),
        "poros":
            executar_seguro(
                "poros",
                poros
            ),
        "linhas_expressao":
            executar_seguro(
                "linhas",
                linhas
            ),
        "tonalidade":
            executar_seguro(
                "tonalidade",
                tonalidade
            ),
        "vermelhidao":
            executar_seguro(
                "vermelhidao",
                vermelhidao
            )
    }

    valores_base: List[float] = []

    for chave in (
        "poros",
        "linhas_expressao",
        "tonalidade"
    ):
        valor = resultado[
            chave
        ].get("valor")

        if isinstance(
            valor,
            (int, float)
        ):
            valores_base.append(
                float(valor)
            )

    if valores_base:
        carga = float(
            np.mean(
                valores_base
            )
        )

        hidratacao = limitar(
            100.0 -
            carga * 0.72
        )

        resultado["hidratacao"] = (
            metrica(
                hidratacao,
                nivel_maior_melhor(
                    hidratacao
                ),
                (
                    "Aparência visual de hidratação "
                    f"estimada em {hidratacao:.0f}/100."
                ),
                min(
                    70.0,
                    qualidade * 0.72
                ),
                (
                    "Não é medição clínica de hidratação. "
                    "Exige equipamento específico."
                )
            )
        )
    else:
        resultado["hidratacao"] = (
            metrica(
                None,
                "NAO_CONCLUSIVO",
                (
                    "A aparência de hidratação "
                    "não pôde ser estimada."
                ),
                0.0,
                (
                    "Não é medição clínica de hidratação."
                )
            )
        )

    return resultado


def montar_resultado(
    qualidade_nome: str,
    qualidade_pontos: float,
    avisos: Sequence[str],
    qualidade_detalhes: Dict[str, float],
    face: FaceData,
    metricas: Dict[str, Dict[str, Any]]
) -> Dict[str, Any]:
    valores_atencao: List[float] = []

    for chave in (
        "oleosidade",
        "poros",
        "linhas_expressao",
        "tonalidade",
        "vermelhidao"
    ):
        valor = metricas[
            chave
        ].get("valor")

        if isinstance(
            valor,
            (int, float)
        ):
            valores_atencao.append(
                float(valor)
            )

    media = (
        float(
            np.mean(
                valores_atencao
            )
        )
        if valores_atencao
        else 50.0
    )

    status = (
        "APARÊNCIA VISUAL EQUILIBRADA"
        if media < 35
        else
        "ATENÇÃO VISUAL MODERADA"
        if media < 62
        else
        "ATENÇÃO VISUAL ELEVADA"
    )

    oleo = metricas[
        "oleosidade"
    ].get("valor")

    hidratacao = metricas[
        "hidratacao"
    ].get("valor")

    if (
        isinstance(
            oleo,
            (int, float)
        ) and
        isinstance(
            hidratacao,
            (int, float)
        ) and
        oleo > 62 and
        hidratacao < 48
    ):
        perfil = (
            "PELE_MISTA_COM_DESIDRATACAO_VISUAL"
        )
    elif (
        isinstance(
            oleo,
            (int, float)
        ) and
        oleo > 62
    ):
        perfil = (
            "TENDENCIA_OLEOSA_VISUAL"
        )
    elif (
        isinstance(
            hidratacao,
            (int, float)
        ) and
        hidratacao < 45
    ):
        perfil = (
            "TENDENCIA_SECA_VISUAL"
        )
    else:
        perfil = (
            "PELE_VISUALMENTE_EQUILIBRADA"
        )

    relatorio = [
        "ANÁLISE HÍBRIDA MEDIAPIPE + OPENCV",
        "",
        (
            f"Qualidade: {qualidade_nome} "
            f"({qualidade_pontos:.0f}/100)"
        ),
        f"Detector: {face.metodo}",
        f"Status visual: {status}",
        f"Perfil visual: {perfil}",
        ""
    ]

    nomes = {
        "oleosidade":
            "Oleosidade",
        "poros":
            "Poros e textura",
        "linhas_expressao":
            "Linhas aparentes",
        "tonalidade":
            "Tonalidade",
        "hidratacao":
            "Hidratação visual",
        "vermelhidao":
            "Vermelhidão"
    }

    for chave, titulo in (
        nomes.items()
    ):
        item = metricas[chave]

        relatorio.append(
            f"{titulo}: "
            f"{item.get('nivel', 'NAO_CONCLUSIVO')} "
            f"({item.get('valor', '—')})"
        )

    if avisos:
        relatorio.extend(
            [
                "",
                "AVISOS DA CAPTURA"
            ]
        )

        relatorio.extend(
            [
                f"• {aviso}"
                for aviso in avisos
            ]
        )

    relatorio.extend(
        [
            "",
            (
                "AVISO: triagem visual por fotografia comum. "
                "Não equivale a diagnóstico dermatológico, "
                "medição instrumental ou indicação de procedimento."
            )
        ]
    )

    confiancas = [
        item.get(
            "confianca",
            0.0
        )
        for item in metricas.values()
        if isinstance(
            item.get(
                "confianca"
            ),
            (int, float)
        )
    ]

    return {
        "sucesso": True,
        "mensagem":
            "Análise híbrida concluída.",
        "motor": VERSAO,
        "rosto_detectado": True,
        "qualidade_imagem":
            qualidade_nome,
        "pontuacao_qualidade":
            round(
                qualidade_pontos,
                2
            ),
        "observacoes_qualidade":
            (
                " ".join(avisos)
                if avisos
                else
                "A fotografia atendeu aos critérios."
            ),
        "metricas_qualidade":
            qualidade_detalhes,
        "detector_facial":
            face.metodo,
        "mediapipe_utilizado":
            face.metodo.startswith(
                "MEDIAPIPE"
            ),
        "modelo_mediapipe":
            str(MODELO_PADRAO),
        "perfil_codigo":
            perfil,
        "perfil_motivo":
            (
                "Classificação visual baseada em brilho, "
                "textura, tonalidade, vermelhidão "
                "e hidratação aparente."
            ),
        "status_visual":
            status,
        "oleosidade":
            metricas["oleosidade"],
        "poros":
            metricas["poros"],
        "linhas_expressao":
            metricas[
                "linhas_expressao"
            ],
        "tonalidade":
            metricas["tonalidade"],
        "hidratacao":
            metricas["hidratacao"],
        "vermelhidao":
            metricas["vermelhidao"],
        "recomendacoes":
            (
                "Manter iluminação, distância e enquadramento "
                "semelhantes nas próximas avaliações. "
                "Alterações persistentes devem ser avaliadas "
                "presencialmente por profissional habilitado."
            ),
        "aviso_importante":
            (
                "Triagem visual; não substitui "
                "avaliação dermatológica."
            ),
        "resultado_completo":
            "\n".join(relatorio),
        "confianca_global":
            round(
                float(
                    np.mean(
                        confiancas
                    )
                )
                if confiancas
                else 0.0,
                2
            )
    }


def main() -> None:
    global ETAPA

    if len(sys.argv) != 2:
        emitir_json(
            False,
            "Uso incorreto do analisador.",
            2
        )

    imagem = carregar_imagem(
        Path(
            sys.argv[1]
        )
    )

    face = detectar_rosto(
        imagem
    )

    mascaras = criar_mascaras(
        imagem,
        face
    )

    (
        qualidade_nome,
        qualidade_pontos,
        avisos,
        qualidade_detalhes
    ) = validar_qualidade(
        imagem,
        face,
        mascaras
    )

    metricas = analisar_metricas(
        imagem,
        mascaras,
        qualidade_pontos,
        face
    )

    ETAPA = "MONTAR_RESULTADO"

    resposta = montar_resultado(
        qualidade_nome,
        qualidade_pontos,
        avisos,
        qualidade_detalhes,
        face,
        metricas
    )

    print(
        json.dumps(
            resposta,
            ensure_ascii=False,
            allow_nan=False
        ),
        flush=True
    )


if __name__ == "__main__":
    try:
        main()

    except SystemExit:
        raise

    except Exception as erro:
        print(
            json.dumps(
                {
                    "sucesso": False,
                    "mensagem":
                        "O analisador encontrou uma falha controlada.",
                    "motor": VERSAO,
                    "etapa": ETAPA,
                    "tipo_erro":
                        type(erro).__name__,
                    "detalhes":
                        str(erro)[:700],
                    "mediapipe_importado":
                        MEDIAPIPE_IMPORTADO,
                    "modelo_existe":
                        MODELO_PADRAO.is_file(),
                    "modelo":
                        str(MODELO_PADRAO),
                    "opencv":
                        getattr(
                            cv2,
                            "__version__",
                            "desconhecido"
                        ),
                    "numpy":
                        getattr(
                            np,
                            "__version__",
                            "desconhecido"
                        ),
                    "trace":
                        traceback.format_exc(
                            limit=8
                        )
                },
                ensure_ascii=False,
                allow_nan=False
            ),
            flush=True
        )

        raise SystemExit(3)
