import argparse
import hashlib
import json
import sys
from dataclasses import dataclass
from pathlib import Path

SCHEMA_VERSION = 1
FACE_PARSER_MANIFEST_SCHEMA_VERSION = 1
FACE_PARSER_MODEL_ID = "PayamFard123/dermaintel-face-parsing/resnet18.onnx"
FACE_PARSER_SOURCE_REVISION = "c1c5fd399cc72b45e3613019879fc6c17a7d8f6a"
FACE_PARSER_EXPECTED_SHA256 = "0d9bd318e46987c3bdbfacae9e2c0f461cae1c6ac6ea6d43bbe541a91727e33f"
FACE_PARSER_SOURCE_URL = f"https://huggingface.co/PayamFard123/dermaintel-face-parsing/resolve/{FACE_PARSER_SOURCE_REVISION}/resnet18.onnx"
FACE_PARSER_FALLBACK_SOURCE = "https://github.com/yakhyo/face-parsing/releases/download/weights/resnet18.onnx"
FACE_PARSER_LICENSE = "MIT model card; weights licensed separately from ChromaFrame code"
FACE_PARSER_LICENSE_ID = "MIT"
FACE_PARSER_PROVENANCE = "PayamFard123/dermaintel-face-parsing resnet18.onnx; yakhyo/face-parsing weights"
FACE_PARSER_INPUT_SIZE = (512, 512)
FACE_PARSER_MEAN = (0.485, 0.456, 0.406)
FACE_PARSER_STD = (0.229, 0.224, 0.225)
FACE_PARSER_CLASS_COUNT = 19

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_BROW = [70, 63, 105, 66, 107, 55, 65, 52]
RIGHT_BROW = [336, 296, 334, 293, 300, 282, 295, 285]
LEFT_EYE = [33, 246, 161, 160, 159, 158, 157, 173, 133, 155, 154, 153, 145, 144, 163, 7]
RIGHT_EYE = [362, 398, 384, 385, 386, 387, 388, 466, 263, 249, 390, 373, 374, 380, 381, 382]
LEFT_IRIS = [468, 469, 470, 471, 472]
RIGHT_IRIS = [473, 474, 475, 476, 477]
LIPS = [61, 146, 91, 181, 84, 17, 314, 405, 321, 375, 291, 409, 270, 269, 267, 0, 37, 39, 40, 185]
BEARD = [172, 136, 150, 149, 176, 148, 152, 377, 400, 378, 379, 365, 397, 288, 57, 212, 202, 204, 194, 201, 200, 421, 418, 424, 422]

COLORS = {
    "skin": "red",
    "brow": "brown",
    "iris": "blue",
    "sclera": "cyan",
    "lip": "magenta",
    "hair": "purple",
    "beard": "orange",
}
DEFAULT_OVERLAY_KINDS = ("skin", "brow", "iris", "sclera", "lip", "hair")

LOW_EVIDENCE_FACIAL_HAIR = ("low_evidence", "clean_shaven_or_low_stubble_evidence", 0.35)
APPROXIMATE_FACIAL_HAIR = ("approximate", "landmark_lower_face_proxy", 0.55)
FACE_PARSER_EXPECTED_LABELS = {
    "background": 0,
    "skin": 1,
    "l_brow": 2,
    "r_brow": 3,
    "l_eye": 4,
    "r_eye": 5,
    "eye_g": 6,
    "l_ear": 7,
    "r_ear": 8,
    "ear_r": 9,
    "nose": 10,
    "mouth": 11,
    "u_lip": 12,
    "l_lip": 13,
    "neck": 14,
    "neck_l": 15,
    "cloth": 16,
    "hair": 17,
    "hat": 18,
}
BASELINE_FACE_PARSER_REJECTED_LABELS = {"beard", "moustache", "mustache", "stubble", "facial_hair"}
PARSER_HAIR_CONFIDENCE = 0.78
PARSER_MASK_QUALITY_THRESHOLDS = {
    "hair": {"min_coverage": 0.02, "min_bbox_fill": 0.25, "max_components": 4, "min_largest_component_share": 0.70},
    "skin": {"min_coverage": 0.03, "min_bbox_fill": 0.35, "max_components": 6, "min_largest_component_share": 0.65},
    "brow": {"min_coverage": 0.001, "min_bbox_fill": 0.15, "max_components": 4, "min_largest_component_share": 0.35},
    "lip": {"min_coverage": 0.001, "min_bbox_fill": 0.15, "max_components": 3, "min_largest_component_share": 0.55},
}


@dataclass(frozen=True)
class Request:
    request_id: str
    image_path: Path
    image_width: int
    image_height: int
    requested_regions: list[str]
    min_region_pixels: int
    erode_pixels: int
    max_faces: int
    mediapipe_task_path: Path
    face_parsing_onnx_path: Path | None
    asset_manifest_path: Path | None
    overlay_output_path: Path | None
    overlay_include_points: bool
    overlay_include_labels: bool


@dataclass(frozen=True)
class FaceParserManifest:
    manifest_schema_version: int
    model_id: str
    sha256: str
    expected_sha256: str
    license: str
    license_id: str
    approved_for_use: bool
    model_type: str
    source_url: str
    source_revision: str
    fallback_source: str
    provenance: str
    labels: dict[str, int]
    input_width: int
    input_height: int
    output_type: str
    mean: tuple[float, float, float]
    std: tuple[float, float, float]


def main() -> int:
    parser = argparse.ArgumentParser(prog="chromaframe_vision_helper")
    subcommands = parser.add_subparsers(dest="command", required=True)
    extract = subcommands.add_parser("extract")
    extract.add_argument("--request-json", required=True)
    extract.add_argument("--response-json", required=True)
    args = parser.parse_args()
    if args.command != "extract":
        return 2
    return run_extract(Path(args.request_json), Path(args.response_json))


def run_extract(request_path: Path, response_path: Path) -> int:
    request_id = "unknown"
    try:
        request = parse_request(request_path)
        request_id = request.request_id
        regions, faces_detected = extract_regions(request)
        warnings = helper_warnings(request, regions)
        region_set = {
            "image_width": request.image_width,
            "image_height": request.image_height,
            "regions": regions,
            "extraction_quality": {
                "faces_detected": faces_detected,
                "selected_face_index": 0,
                "backend": "mediapipe_face_landmarker",
                "warnings": warnings,
            },
            "warnings": warnings,
        }
        overlay = write_overlay_if_requested(request, regions)
        write_response(response_path, ok_response(request.request_id, region_set, overlay))
        return 0
    except HelperError as error:
        write_response(response_path, error_response(error.request_id or request_id, error.code))
        return 0
    except Exception:
        write_response(response_path, error_response(request_id, "extraction_failed"))
        return 0


def parse_request(path: Path) -> Request:
    data = json.loads(path.read_text())
    request_id = str(data.get("request_id", "unknown"))
    if data.get("schema_version") != SCHEMA_VERSION:
        raise HelperError("invalid_request", request_id)
    image_path = Path(data["image_path"])
    task_path = Path(data["mediapipe_task_path"])
    if not image_path.is_file():
        raise HelperError("invalid_request", request_id)
    if not task_path.is_file():
        raise HelperError("model_missing", request_id)
    parser_path = Path(data["face_parsing_onnx_path"]) if data.get("face_parsing_onnx_path") else None
    asset_manifest_path = Path(data["asset_manifest_path"]) if data.get("asset_manifest_path") else None
    if parser_path is None or not parser_path.is_file():
        raise HelperError("model_missing", request_id)
    if asset_manifest_path is None or not asset_manifest_path.is_file():
        raise HelperError("invalid_request", request_id)
    overlay_path = Path(data["overlay_output_path"]) if data.get("overlay_output_path") else None
    if overlay_path is not None and not overlay_path.is_absolute():
        raise HelperError("invalid_request", request_id)
    image_width, image_height = oriented_image_dimensions(image_path)
    return Request(
        request_id=request_id,
        image_path=image_path,
        image_width=image_width,
        image_height=image_height,
        requested_regions=list(data.get("requested_regions", [])),
        min_region_pixels=int(data.get("min_region_pixels", 20)),
        erode_pixels=int(data.get("erode_pixels", 0)),
        max_faces=int(data.get("max_faces", 1)),
        mediapipe_task_path=task_path,
        face_parsing_onnx_path=parser_path,
        asset_manifest_path=asset_manifest_path,
        overlay_output_path=overlay_path,
        overlay_include_points=bool(data.get("overlay_include_points", False)),
        overlay_include_labels=bool(data.get("overlay_include_labels", False)),
    )


def extract_regions(request: Request) -> tuple[list[dict], int]:
    if sys.version_info[:2] not in ((3, 11), (3, 12)):
        raise HelperError("backend_version_mismatch")
    try:
        import mediapipe as mp
        import numpy as np
        from mediapipe.tasks import python
        from mediapipe.tasks.python import vision
    except Exception as exc:
        raise HelperError("package_missing") from exc
    base_options = python.BaseOptions(model_asset_path=str(request.mediapipe_task_path))
    detection_limit = max(request.max_faces + 1, 2)
    options = vision.FaceLandmarkerOptions(base_options=base_options, num_faces=detection_limit)
    detector = vision.FaceLandmarker.create_from_options(options)
    mp_image = mp.Image(image_format=mp.ImageFormat.SRGB, data=np.asarray(load_oriented_rgb_image(request.image_path)))
    result = detector.detect(mp_image)
    faces_detected = len(result.face_landmarks)
    if faces_detected == 0:
        raise HelperError("no_face")
    if faces_detected > request.max_faces:
        raise HelperError("multiple_faces")
    landmarks = result.face_landmarks[0]
    regions = []
    requested = set(request.requested_regions)
    add_measured_regions(regions, requested, request, landmarks)
    add_parser_regions(regions, requested, request, landmarks)
    add_not_measured_regions(regions, requested)
    return regions, faces_detected


def add_measured_regions(regions: list[dict], requested: set[str], request: Request, landmarks) -> None:
    skin = skin_polygons(landmarks, request)
    if "skin" in requested:
        add_region(regions, "skin", skin, request, "measured", "mediapipe_landmarks", None)
    if "brow" in requested:
        add_region(regions, "brow", [polygon(landmarks, LEFT_BROW, request), polygon(landmarks, RIGHT_BROW, request)], request, "measured", "mediapipe_landmarks", None)
    if "iris" in requested:
        add_region(regions, "iris", [polygon(landmarks, LEFT_IRIS, request), polygon(landmarks, RIGHT_IRIS, request)], request, "measured", "mediapipe_landmarks", None)
    if "sclera" in requested:
        add_region(regions, "sclera", sclera_polygons(landmarks, request), request, "measured", "mediapipe_landmarks", None)
    if "lip" in requested:
        add_region(regions, "lip", [polygon(landmarks, LIPS, request)], request, "measured", "mediapipe_landmarks", None)
    if "beard" in requested:
        beard = [polygon(landmarks, BEARD, request)]
        status, reason, confidence = classify_facial_hair_proxy(request, skin, beard)
        add_region(regions, "beard", beard, request, status, "approximation", reason, confidence)


def add_not_measured_regions(regions: list[dict], requested: set[str]) -> None:
    observed = {region.get("kind") for region in regions}
    if "hair" in requested and "hair" not in observed:
        regions.append(not_measured("hair", "semantic_parser_hair_evidence_low"))
    if "clothing" in requested and "clothing" not in observed:
        regions.append(not_measured("clothing", "portrait_mvp_not_supported"))


def add_parser_regions(regions: list[dict], requested: set[str], request: Request, landmarks=None) -> None:
    manifest = load_face_parser_manifest(request)
    parser_masks = run_face_parser(request, manifest, landmarks)
    for kind, polygons in parser_masks.items():
        if kind not in requested:
            continue
        if kind == "beard":
            continue
        if kind == "hair" and not polygons:
            replace_region(regions, not_measured("hair", "semantic_parser_hair_evidence_low"))
            continue
        if kind == "hair":
            replace_region(regions, parser_observation(kind, polygons, request))
            continue
        if kind in {"brow", "skin", "lip"}:
            parser_region = measured_parser_observation(kind, polygons, request)
            if parser_region is not None:
                replace_region(regions, parser_region)


def replace_region(regions: list[dict], replacement: dict) -> None:
    regions[:] = [region for region in regions if region.get("kind") != replacement.get("kind")]
    regions.append(replacement)


def parser_observation(kind: str, polygons: list[list[dict]], request: Request) -> dict:
    clean = [poly for poly in polygons if len(poly) >= 3 and polygon_area(poly) >= request.min_region_pixels / max(len(polygons), 1)]
    area = sum(polygon_area(poly) for poly in clean)
    if area < request.min_region_pixels:
        return not_measured(kind, f"semantic_parser_{kind}_evidence_low")
    return observation(kind, "measured", "parser_mask", clean, None, int(area), PARSER_HAIR_CONFIDENCE)


def measured_parser_observation(kind: str, polygons: list[list[dict]], request: Request) -> dict | None:
    clean = [poly for poly in polygons if len(poly) >= 3 and polygon_area(poly) >= request.min_region_pixels / max(len(polygons), 1)]
    area = sum(polygon_area(poly) for poly in clean)
    if area < request.min_region_pixels:
        return None
    return observation(kind, "measured", "parser_mask", clean, None, int(area), PARSER_HAIR_CONFIDENCE)


def polygon(landmarks, indices: list[int], request: Request) -> list[dict]:
    points = []
    for index in indices:
        landmark = landmarks[index]
        x = clamp(landmark.x * request.image_width, 0.0, request.image_width - 1.0)
        y = clamp(landmark.y * request.image_height, 0.0, request.image_height - 1.0)
        points.append({"x": x, "y": y})
    return shrink_polygon(points, float(request.erode_pixels))


def skin_polygons(landmarks, request: Request) -> list[list[dict]]:
    forehead = polygon(landmarks, [103, 67, 109, 10, 338, 297, 332, 333, 299, 69], request)
    left_cheek = polygon(landmarks, [117, 123, 50, 187, 207, 216, 172, 136, 150, 101, 118], request)
    right_cheek = polygon(landmarks, [346, 352, 280, 411, 427, 436, 397, 365, 379, 330, 347], request)
    nose_bridge = polygon(landmarks, [168, 193, 122, 196, 419, 351, 417], request)
    return [forehead, left_cheek, right_cheek, nose_bridge]


def sclera_polygons(landmarks, request: Request) -> list[list[dict]]:
    return eye_sclera_patches(landmarks, LEFT_EYE, LEFT_IRIS, request) + eye_sclera_patches(landmarks, RIGHT_EYE, RIGHT_IRIS, request)


def eye_sclera_patches(landmarks, eye_indices: list[int], iris_indices: list[int], request: Request) -> list[list[dict]]:
    eye = polygon(landmarks, eye_indices, request)
    iris = polygon(landmarks, iris_indices, request)
    eye_box = bounds(eye)
    iris_box = bounds(iris)
    y_top = eye_box[1] + (eye_box[3] - eye_box[1]) * 0.25
    y_bottom = eye_box[1] + (eye_box[3] - eye_box[1]) * 0.75
    left = rectangle(eye_box[0], y_top, iris_box[0] - request.erode_pixels, y_bottom, request)
    right = rectangle(iris_box[2] + request.erode_pixels, y_top, eye_box[2], y_bottom, request)
    return [poly for poly in [left, right] if polygon_area(poly) > 0.0]


def add_region(regions: list[dict], kind: str, polygons: list[list[dict]], request: Request, status: str, source: str, reason: str | None, confidence: float | None = None) -> None:
    clean = [poly for poly in polygons if len(poly) >= 3 and polygon_area(poly) >= request.min_region_pixels / max(len(polygons), 1)]
    area = sum(polygon_area(poly) for poly in clean)
    if area < request.min_region_pixels:
        regions.append(not_measured(kind, "region_area_too_small"))
        return
    regions.append(observation(kind, status, source, clean, reason, int(area), confidence))


def not_measured(kind: str, reason: str) -> dict:
    return {
        "kind": kind,
        "status": {"not_measured": {"reason": reason}},
        "source": "not_measured",
        "confidence": 0.0,
        "mask": None,
        "sample_hint": None,
        "approximate_reason": None,
        "not_measured_reason": reason,
    }


def observation(kind: str, status: str, source: str, polygons: list[list[dict]], reason: str | None, sample_hint: int, confidence: float | None = None) -> dict:
    if confidence is None:
        confidence = 0.55 if status == "approximate" else 0.88
    return {
        "kind": kind,
        "status": status,
        "source": source,
        "confidence": confidence,
        "mask": {"encoding": "polygon", "polygons": polygons},
        "sample_hint": sample_hint,
        "approximate_reason": reason,
        "not_measured_reason": None,
    }


def oriented_image_dimensions(image_path: Path) -> tuple[int, int]:
    image = load_oriented_rgb_image(image_path)
    return image.size


def load_oriented_rgb_image(image_path: Path):
    from PIL import Image, ImageOps

    with Image.open(image_path) as image:
        return ImageOps.exif_transpose(image).convert("RGB")


def load_face_parser_manifest(request: Request) -> FaceParserManifest:
    if request.face_parsing_onnx_path is None or not request.face_parsing_onnx_path.is_file():
        raise HelperError("model_missing", request.request_id)
    if request.asset_manifest_path is None or not request.asset_manifest_path.is_file():
        raise HelperError("invalid_request", request.request_id)

    try:
        data = json.loads(request.asset_manifest_path.read_text())
        entry = required_dict(data, "face_parsing_onnx")
        labels = parse_parser_labels(required_dict(entry, "labels"))
        manifest = FaceParserManifest(
            manifest_schema_version=required_positive_int(entry, "manifest_schema_version"),
            model_id=required_string(entry, "model_id"),
            sha256=required_string(entry, "sha256").lower(),
            expected_sha256=required_string(entry, "expected_sha256").lower(),
            license=required_string(entry, "license"),
            license_id=required_string(entry, "license_id"),
            approved_for_use=required_approved_true(entry),
            model_type=required_string(entry, "model_type"),
            source_url=required_string(entry, "source_url"),
            source_revision=required_string(entry, "source_revision"),
            fallback_source=required_string(entry, "fallback_source"),
            provenance=required_string(entry, "provenance"),
            labels=labels,
            input_width=required_positive_int(required_dict(entry, "input"), "width"),
            input_height=required_positive_int(required_dict(entry, "input"), "height"),
            output_type=required_string(required_dict(entry, "output"), "type"),
            mean=required_float_triplet(required_dict(entry, "normalization"), "mean"),
            std=required_float_triplet(required_dict(entry, "normalization"), "std"),
        )
    except Exception as exc:
        raise HelperError("invalid_request", request.request_id) from exc

    if not is_approved_face_parser_manifest(manifest):
        raise HelperError("invalid_request", request.request_id)
    if sha256_file(request.face_parsing_onnx_path) != manifest.sha256:
        raise HelperError("model_missing", request.request_id)
    return manifest


def parse_parser_labels(raw_labels) -> dict[str, int]:
    if not isinstance(raw_labels, dict):
        raise ValueError("labels must be a map")
    labels: dict[str, int] = {}
    for label, value in raw_labels.items():
        if not isinstance(label, str) or not label:
            raise ValueError("label must be a non-empty string")
        if not isinstance(value, int) or isinstance(value, bool) or value < 0:
            raise ValueError("label id must be a non-negative integer")
        labels[label] = value
    return labels


def required_dict(data: dict, key: str) -> dict:
    value = data[key]
    if not isinstance(value, dict):
        raise ValueError(f"{key} must be an object")
    return value


def required_string(data: dict, key: str) -> str:
    value = data[key]
    if not isinstance(value, str) or not value.strip():
        raise ValueError(f"{key} must be a non-empty string")
    return value.strip()


def required_positive_int(data: dict, key: str) -> int:
    value = data[key]
    if not isinstance(value, int) or isinstance(value, bool) or value <= 0:
        raise ValueError(f"{key} must be a positive integer")
    return value


def required_approved_true(data: dict) -> bool:
    value = data["approved_for_use"]
    if value is not True:
        raise ValueError("approved_for_use must be boolean true")
    return value


def required_float_triplet(data: dict, key: str) -> tuple[float, float, float]:
    value = data[key]
    if not isinstance(value, list) or len(value) != 3:
        raise ValueError(f"{key} must be a three-value list")
    if not all(isinstance(item, (int, float)) and not isinstance(item, bool) for item in value):
        raise ValueError(f"{key} must contain numbers")
    triplet = tuple(float(item) for item in value)
    if any(item <= 0.0 for item in triplet) and key == "std":
        raise ValueError("std values must be positive")
    return triplet


def is_approved_face_parser_manifest(manifest: FaceParserManifest) -> bool:
    if not manifest.approved_for_use:
        return False
    if manifest.model_type != "baseline_face_parser":
        return False
    if manifest.manifest_schema_version != FACE_PARSER_MANIFEST_SCHEMA_VERSION:
        return False
    if manifest.model_id != FACE_PARSER_MODEL_ID:
        return False
    if manifest.source_revision != FACE_PARSER_SOURCE_REVISION:
        return False
    if manifest.source_url != FACE_PARSER_SOURCE_URL:
        return False
    if manifest.fallback_source != FACE_PARSER_FALLBACK_SOURCE:
        return False
    if manifest.license != FACE_PARSER_LICENSE or manifest.license_id != FACE_PARSER_LICENSE_ID:
        return False
    if manifest.provenance != FACE_PARSER_PROVENANCE:
        return False
    if manifest.expected_sha256 != FACE_PARSER_EXPECTED_SHA256 or manifest.sha256 != FACE_PARSER_EXPECTED_SHA256:
        return False
    if not manifest.sha256 or any(character not in "0123456789abcdef" for character in manifest.sha256) or len(manifest.sha256) != 64:
        return False
    if (manifest.input_width, manifest.input_height) != FACE_PARSER_INPUT_SIZE:
        return False
    if not float_triplet_matches(manifest.mean, FACE_PARSER_MEAN) or not float_triplet_matches(manifest.std, FACE_PARSER_STD):
        return False
    if manifest.output_type != "logits_argmax":
        return False
    if set(manifest.labels) & BASELINE_FACE_PARSER_REJECTED_LABELS:
        return False
    return manifest.labels == FACE_PARSER_EXPECTED_LABELS


def float_triplet_matches(actual: tuple[float, float, float], expected: tuple[float, float, float]) -> bool:
    return all(abs(left - right) <= 1e-6 for left, right in zip(actual, expected))


def sha256_file(path: Path) -> str:
    digest = hashlib.sha256()
    with path.open("rb") as handle:
        for chunk in iter(lambda: handle.read(1024 * 1024), b""):
            digest.update(chunk)
    return digest.hexdigest()


def run_face_parser(request: Request, manifest: FaceParserManifest, landmarks=None) -> dict[str, list[list[dict]]]:
    try:
        import numpy as np
        import onnxruntime as ort
    except Exception as exc:
        raise HelperError("package_missing", request.request_id) from exc

    if hasattr(ort, "disable_telemetry_events"):
        ort.disable_telemetry_events()
    session = ort.InferenceSession(str(request.face_parsing_onnx_path), providers=["CPUExecutionProvider"])
    input_name = session.get_inputs()[0].name
    parser_input = prepare_parser_input(request.image_path, manifest, np)
    outputs = session.run(None, {input_name: parser_input})
    class_map = parser_class_map(outputs[0], manifest, np)
    return parser_masks_from_class_map(class_map, manifest, request, landmarks)


def prepare_parser_input(image_path: Path, manifest: FaceParserManifest, np):
    resized = load_oriented_rgb_image(image_path).resize((manifest.input_width, manifest.input_height))
    array = np.asarray(resized, dtype=np.float32) / 255.0
    mean = np.asarray(manifest.mean, dtype=np.float32)
    std = np.asarray(manifest.std, dtype=np.float32)
    array = (array - mean) / std
    return array.transpose(2, 0, 1)[None, ...]


def parser_class_map(output, manifest: FaceParserManifest, np):
    array = np.asarray(output)
    if manifest.output_type != "logits_argmax":
        raise HelperError("invalid_request")
    if array.ndim != 4 or array.shape[0] != 1 or array.shape[1] != FACE_PARSER_CLASS_COUNT:
        raise HelperError("invalid_request")
    return np.argmax(array[0], axis=0)


def parser_masks_from_class_map(class_map, manifest: FaceParserManifest, request: Request, landmarks=None) -> dict[str, list[list[dict]]]:
    import cv2
    import numpy as np

    masks: dict[str, list[list[dict]]] = {}
    label_groups = {
        "hair": ["hair"],
        "brow": ["l_brow", "r_brow"],
        "skin": ["skin"],
        "lip": ["mouth", "u_lip", "l_lip"],
    }
    class_ids = cv2.resize(
        np.asarray(class_map).astype("int32"),
        (request.image_width, request.image_height),
        interpolation=cv2.INTER_NEAREST,
    )
    skin_exclusion_mask = parser_skin_exclusion_mask(class_ids, manifest, request, landmarks)
    for kind, labels in label_groups.items():
        selected_ids = [manifest.labels[label] for label in labels if label in manifest.labels]
        if not selected_ids:
            continue
        mask = np.isin(class_ids, selected_ids)
        masks[kind] = polygons_from_parser_mask(kind, mask, request, skin_exclusion_mask if kind == "skin" else None, landmarks)
    return masks


def polygons_from_parser_mask(kind: str, mask, request: Request, exclusion_mask=None, landmarks=None) -> list[list[dict]]:
    import cv2
    import numpy as np

    if not parser_mask_quality_passes(kind, mask):
        return []
    if kind == "skin":
        return skin_sample_polygons_from_parser_mask(mask, request, exclusion_mask, landmarks)
    mask_array = np.asarray(mask, dtype=np.uint8)
    contours, _ = cv2.findContours(mask_array, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_TC89_KCOS)
    polygons = [parser_contour_polygon(contour, request) for contour in contours]
    clean = [polygon for polygon in polygons if len(polygon) >= 3 and polygon_area(polygon) > 0.0]
    return sorted(clean, key=polygon_area, reverse=True)


def skin_sample_polygons_from_parser_mask(mask, request: Request, exclusion_mask=None, landmarks=None) -> list[list[dict]]:
    import numpy as np

    mask_array = np.asarray(mask, dtype=bool)
    if exclusion_mask is not None:
        mask_array = mask_array & ~np.asarray(exclusion_mask, dtype=bool)
    ys, xs = np.where(mask_array)
    if len(xs) == 0 or len(ys) == 0:
        return []
    left, right = int(xs.min()), int(xs.max()) + 1
    top, bottom = int(ys.min()), int(ys.max()) + 1
    width = max(right - left, 1)
    height = max(bottom - top, 1)
    patch_boxes = landmark_skin_patch_boxes(landmarks, request) if landmarks is not None else []
    if not patch_boxes:
        patch_boxes = [
            (left + width * 0.34, top + height * 0.08, width * 0.32, height * 0.13),
            (left + width * 0.15, top + height * 0.42, width * 0.24, height * 0.20),
            (left + width * 0.61, top + height * 0.42, width * 0.24, height * 0.20),
            (left + width * 0.42, top + height * 0.46, width * 0.16, height * 0.16),
            (left + width * 0.36, top + height * 0.74, width * 0.28, height * 0.12),
        ]
    patch_groups = [parser_mask_patch(mask_array, x, y, patch_width, patch_height, request) for x, y, patch_width, patch_height in patch_boxes]
    patches = [patch for group in patch_groups for patch in group]
    return [patch for patch in patches if len(patch) >= 3 and polygon_area(patch) >= request.min_region_pixels]


def landmark_skin_patch_boxes(landmarks, request: Request) -> list[tuple[float, float, float, float]]:
    face_left, face_top, face_right, face_bottom = bounds(polygon(landmarks, FACE_OVAL, request))
    left_eye = bounds(polygon(landmarks, LEFT_EYE, request))
    right_eye = bounds(polygon(landmarks, RIGHT_EYE, request))
    left_brow = bounds(polygon(landmarks, LEFT_BROW, request))
    right_brow = bounds(polygon(landmarks, RIGHT_BROW, request))
    lip_left, lip_top, lip_right, lip_bottom = bounds(polygon(landmarks, LIPS, request))
    face_width = max(face_right - face_left, 1.0)
    face_height = max(face_bottom - face_top, 1.0)
    eye_bottom = max(left_eye[3], right_eye[3])
    brow_top = min(left_brow[1], right_brow[1])
    margin = face_height * 0.04
    cheek_top = eye_bottom + margin
    cheek_bottom = max(cheek_top + face_height * 0.07, lip_top - margin)
    nose_top = eye_bottom + margin
    nose_bottom = max(nose_top + face_height * 0.06, lip_top - margin)
    chin_top = lip_bottom + margin
    chin_bottom = max(chin_top + face_height * 0.06, face_bottom - margin * 1.5)
    return [
        (face_left + face_width * 0.34, face_top + face_height * 0.05, face_width * 0.32, max(brow_top - face_top - margin, face_height * 0.06)),
        (face_left + face_width * 0.10, cheek_top, face_width * 0.28, cheek_bottom - cheek_top),
        (face_left + face_width * 0.62, cheek_top, face_width * 0.28, cheek_bottom - cheek_top),
        (face_left + face_width * 0.43, nose_top, face_width * 0.14, nose_bottom - nose_top),
        (face_left + face_width * 0.36, chin_top, face_width * 0.28, chin_bottom - chin_top),
    ]


def parser_mask_patch(mask_array, x: float, y: float, width: float, height: float, request: Request) -> list[list[dict]]:
    import numpy as np

    left = max(int(round(x)), 0)
    top = max(int(round(y)), 0)
    right = min(int(round(x + width)), mask_array.shape[1])
    bottom = min(int(round(y + height)), mask_array.shape[0])
    if right <= left or bottom <= top:
        return []
    patch_mask = np.zeros(mask_array.shape, dtype=bool)
    patch_mask[top:bottom, left:right] = np.asarray(mask_array[top:bottom, left:right], dtype=bool)
    polygon = largest_safe_rectangle_from_mask(patch_mask, request)
    return [polygon] if polygon else []


def largest_safe_rectangle_from_mask(mask_array, request: Request) -> list[dict]:
    import numpy as np

    safe_mask = np.asarray(mask_array, dtype=bool)
    ys, xs = np.where(safe_mask)
    if len(xs) == 0 or len(ys) == 0:
        return []
    min_width = max(3, int(round(request.image_width * 0.025)))
    min_height = max(3, int(round(request.image_height * 0.018)))
    best = None
    best_area = 0
    for top in range(int(ys.min()), int(ys.max()) + 1):
        safe_columns = np.ones(safe_mask.shape[1], dtype=bool)
        for bottom in range(top, int(ys.max()) + 1):
            safe_columns &= safe_mask[bottom, :]
            height = bottom - top + 1
            if height < min_height:
                continue
            for left, right in contiguous_true_runs(safe_columns):
                width = right - left
                area = width * height
                if width >= min_width and area > best_area:
                    best = (left, top, right - 1, bottom)
                    best_area = area
    if best is None:
        return []
    return rectangle(float(best[0]), float(best[1]), float(best[2]), float(best[3]), request)


def contiguous_true_runs(values) -> list[tuple[int, int]]:
    runs = []
    start = None
    for index, value in enumerate(values):
        if value and start is None:
            start = index
            continue
        if not value and start is not None:
            runs.append((start, index))
            start = None
    if start is not None:
        runs.append((start, len(values)))
    return runs


def parser_skin_exclusion_mask(class_ids, manifest: FaceParserManifest, request: Request, landmarks=None):
    import cv2
    import numpy as np

    excluded_labels = ["hair", "hat", "l_brow", "r_brow", "l_eye", "r_eye", "eye_g", "mouth", "u_lip", "l_lip"]
    excluded_ids = [manifest.labels[label] for label in excluded_labels if label in manifest.labels]
    exclusion = np.isin(class_ids, excluded_ids) if excluded_ids else np.zeros(class_ids.shape, dtype=bool)
    if landmarks is not None:
        exclusion |= landmark_feature_mask(landmarks, request, class_ids.shape)
    kernel_size = max(5, int(round(min(request.image_width, request.image_height) * 0.018)))
    if kernel_size % 2 == 0:
        kernel_size += 1
    kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (kernel_size, kernel_size))
    return cv2.dilate(exclusion.astype("uint8"), kernel, iterations=1).astype(bool)


def landmark_feature_mask(landmarks, request: Request, shape) -> object:
    import cv2
    import numpy as np

    mask = np.zeros(shape, dtype=np.uint8)
    feature_indices = [LEFT_BROW, RIGHT_BROW, LEFT_EYE, RIGHT_EYE, LEFT_IRIS, RIGHT_IRIS, LIPS]
    for indices in feature_indices:
        points = np.array(
            [[point["x"], point["y"]] for point in polygon(landmarks, indices, request)],
            dtype=np.int32,
        )
        if len(points) >= 3:
            cv2.fillPoly(mask, [points], 1)
    return mask.astype(bool)


def parser_contour_polygon(contour, request: Request) -> list[dict]:
    import cv2

    if contour is None or len(contour) < 3:
        return []
    perimeter = cv2.arcLength(contour, True)
    epsilon = max(0.75, perimeter * 0.0015)
    approximated = cv2.approxPolyDP(contour, epsilon, True)
    points = approximated.reshape(-1, 2)
    return [
        {
            "x": clamp(float(x), 0.0, request.image_width - 1.0),
            "y": clamp(float(y), 0.0, request.image_height - 1.0),
        }
        for x, y in points
    ]


def parser_mask_quality_passes(kind: str, mask) -> bool:
    import cv2
    import numpy as np

    thresholds = PARSER_MASK_QUALITY_THRESHOLDS.get(kind)
    if thresholds is None:
        return False
    mask_array = np.asarray(mask, dtype=bool)
    if mask_array.ndim != 2 or mask_array.size == 0:
        return False
    foreground_pixels = int(mask_array.sum())
    if foreground_pixels == 0:
        return False
    coverage = foreground_pixels / float(mask_array.size)
    if coverage < thresholds["min_coverage"]:
        return False

    ys, xs = np.where(mask_array)
    bbox_area = int((xs.max() - xs.min() + 1) * (ys.max() - ys.min() + 1))
    if bbox_area <= 0 or foreground_pixels / float(bbox_area) < thresholds["min_bbox_fill"]:
        return False

    component_count, _, stats, _ = cv2.connectedComponentsWithStats(mask_array.astype("uint8"), 8)
    foreground_component_count = component_count - 1
    if foreground_component_count <= 0 or foreground_component_count > thresholds["max_components"]:
        return False
    largest_component_area = int(stats[1:, cv2.CC_STAT_AREA].max())
    largest_component_share = largest_component_area / float(foreground_pixels)
    return largest_component_share >= thresholds["min_largest_component_share"]


def classify_facial_hair_proxy(request: Request, skin_polygons: list[list[dict]], beard_polygons: list[list[dict]]) -> tuple[str, str, float]:
    try:
        evidence = facial_hair_proxy_evidence(load_oriented_rgb_image(request.image_path), skin_polygons, beard_polygons)
    except Exception:
        return LOW_EVIDENCE_FACIAL_HAIR

    if evidence is None:
        return LOW_EVIDENCE_FACIAL_HAIR

    if has_textured_lower_face_evidence(evidence):
        return APPROXIMATE_FACIAL_HAIR
    return LOW_EVIDENCE_FACIAL_HAIR


def facial_hair_proxy_evidence(image, skin_polygons: list[list[dict]], beard_polygons: list[list[dict]]) -> dict | None:
    from PIL import ImageFilter

    skin = mean_rgb_for_polygons(image, skin_polygons)
    beard = mean_rgb_for_polygons(image, beard_polygons)
    if skin is None or beard is None:
        return None

    skin_texture = luma_texture_for_polygons(image, skin_polygons)
    beard_texture = luma_texture_for_polygons(image, beard_polygons)
    if skin_texture is None or beard_texture is None:
        return None

    return {
        "color_distance": rgb_distance(skin, beard),
        "luma_contrast": abs(relative_luma(skin) - relative_luma(beard)) / 255.0,
        "beard_residual_mean": beard_texture["residual_mean"],
        "skin_residual_mean": skin_texture["residual_mean"],
        "beard_residual_p90": beard_texture["residual_p90"],
    }


def has_textured_lower_face_evidence(evidence: dict) -> bool:
    # Color/luma shifts alone are often jaw shadow or makeup. Require local texture
    # stronger than adjacent skin before treating the lower-face proxy as useful
    # facial-hair evidence. Otherwise bias to low evidence and keep it out of scoring.
    has_clear_color_or_luma_shift = evidence["color_distance"] >= 30.0 or evidence["luma_contrast"] >= 0.065
    if not has_clear_color_or_luma_shift:
        return False
    if evidence["skin_residual_mean"] <= 0.0:
        return False

    texture_ratio = evidence["beard_residual_mean"] / evidence["skin_residual_mean"]
    texture_delta = evidence["beard_residual_mean"] - evidence["skin_residual_mean"]
    return texture_ratio >= 1.35 and texture_delta >= 1.5 and evidence["beard_residual_p90"] >= 10.0


def mean_rgb_for_polygons(image, polygons: list[list[dict]]) -> tuple[float, float, float] | None:
    from PIL import Image, ImageDraw
    mask = Image.new("1", image.size, 0)
    draw = ImageDraw.Draw(mask)
    for poly in polygons:
        if len(poly) >= 3:
            draw.polygon([(point["x"], point["y"]) for point in poly], fill=1)
    pixels = image.load()
    mask_pixels = mask.load()
    red = green = blue = count = 0
    width, height = image.size
    for y in range(height):
        for x in range(width):
            if not mask_pixels[x, y]:
                continue
            r, g, b = pixels[x, y]
            red += r
            green += g
            blue += b
            count += 1
    if count < 20:
        return None
    return red / count, green / count, blue / count


def luma_texture_for_polygons(image, polygons: list[list[dict]]) -> dict | None:
    from PIL import Image, ImageDraw, ImageFilter
    mask = Image.new("1", image.size, 0)
    draw = ImageDraw.Draw(mask)
    for poly in polygons:
        if len(poly) >= 3:
            draw.polygon([(point["x"], point["y"]) for point in poly], fill=1)

    gray = image.convert("L")
    blurred = gray.filter(ImageFilter.GaussianBlur(radius=3))
    gray_pixels = gray.load()
    blurred_pixels = blurred.load()
    mask_pixels = mask.load()
    residuals = []
    width, height = image.size
    for y in range(height):
        for x in range(width):
            if mask_pixels[x, y]:
                residuals.append(abs(gray_pixels[x, y] - blurred_pixels[x, y]))
    if len(residuals) < 20:
        return None
    residuals.sort()
    return {
        "residual_mean": sum(residuals) / len(residuals),
        "residual_p90": residuals[int((len(residuals) - 1) * 0.9)],
    }


def rgb_distance(left: tuple[float, float, float], right: tuple[float, float, float]) -> float:
    return sum((a - b) ** 2 for a, b in zip(left, right)) ** 0.5


def relative_luma(rgb: tuple[float, float, float]) -> float:
    red, green, blue = rgb
    return 0.2126 * red + 0.7152 * green + 0.0722 * blue


def write_overlay_if_requested(request: Request, regions: list[dict]) -> dict | None:
    if request.overlay_output_path is None:
        return None
    try:
        from PIL import ImageDraw
        ensure_safe_overlay_target(request.overlay_output_path)
        sidecar_path = overlay_sidecar_path(request.overlay_output_path)
        ensure_safe_overlay_target(sidecar_path)
        image = load_oriented_rgb_image(request.image_path)
        label_draw = ImageDraw.Draw(image, "RGBA")
        for region in regions:
            if not is_drawable_overlay_region(region):
                continue
            mask = region.get("mask")
            if not mask:
                continue
            color = overlay_color(region["kind"])
            draw_region_outline(image, mask["polygons"], color, request.overlay_include_points)
            if request.overlay_include_labels:
                label_point = first_polygon_point(mask)
                if label_point:
                    label_draw.text(label_point, region["kind"], fill=color)
        request.overlay_output_path.parent.mkdir(parents=True, exist_ok=True)
        with request.overlay_output_path.open("xb") as output:
            image.save(output, format=overlay_image_format(request.overlay_output_path))
        write_overlay_sidecar(sidecar_path, request, regions)
    except Exception as exc:
        raise HelperError("extraction_failed") from exc
    return {
        "output_path_redacted": "[REDACTED]",
        "dimensions": [request.image_width, request.image_height],
        "region_count": len(regions),
        "color_key": default_overlay_color_key(),
    }


def is_drawable_overlay_region(region: dict) -> bool:
    if region.get("kind") not in DEFAULT_OVERLAY_KINDS:
        return False
    return region.get("status") == "measured"


def default_overlay_color_key() -> list[dict]:
    return [{"kind": key, "color": COLORS[key]} for key in DEFAULT_OVERLAY_KINDS]


def draw_region_outline(image, polygons: list[list[dict]], color: tuple[int, int, int, int], include_points: bool) -> None:
    import cv2
    import numpy as np

    overlay = np.asarray(image.convert("RGB")).copy()
    filled_mask = np.zeros((image.height, image.width), dtype=np.uint8)
    stroke_width = overlay_stroke_width(image.size)
    for poly in polygons:
        points = np.array([[round(point["x"]), round(point["y"])] for point in poly], dtype=np.int32)
        if len(points) < 3:
            continue
        cv2.fillPoly(filled_mask, [points], 1)
        if include_points:
            point_radius = max(2, stroke_width)
            for x, y in points:
                cv2.circle(overlay, (int(x), int(y)), point_radius, color[:3], thickness=-1, lineType=cv2.LINE_8)
    if filled_mask.any():
        kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (stroke_width, stroke_width))
        boundary = cv2.morphologyEx(filled_mask, cv2.MORPH_GRADIENT, kernel)
        overlay[boundary > 0] = color[:3]
    image.paste(image_from_rgb_array(overlay))


def image_from_rgb_array(array):
    from PIL import Image

    return Image.fromarray(array, mode="RGB")


def overlay_stroke_width(size: tuple[int, int]) -> int:
    return max(3, int(round(min(size) / 350)))


def overlay_color(kind: str) -> tuple[int, int, int, int]:
    return {
        "skin": (255, 0, 0, 180),
        "brow": (90, 50, 20, 180),
        "iris": (0, 80, 255, 180),
        "sclera": (0, 255, 255, 180),
        "lip": (255, 0, 255, 180),
        "hair": (128, 0, 255, 180),
        "beard": (255, 128, 0, 180),
    }.get(kind, (255, 255, 255, 180))


def helper_warnings(request: Request, regions: list[dict]) -> list[str]:
    warnings = []
    for region in regions:
        if region.get("kind") != "beard":
            continue
        if region.get("status") == "approximate":
            warnings.append("beard_landmark_proxy_approximate")
        if region.get("status") == "low_evidence":
            warnings.append("facial_hair_evidence_low")
    if request.face_parsing_onnx_path is None:
        warnings.append("semantic_parser_not_configured_hair_not_measured")
    return warnings


def ensure_safe_overlay_target(path: Path) -> None:
    if path.is_symlink():
        raise HelperError("invalid_request")
    if path.exists():
        raise HelperError("invalid_request")
    parent = path.parent
    if parent.exists() and parent.is_symlink():
        raise HelperError("invalid_request")


def overlay_sidecar_path(path: Path) -> Path:
    suffix = f"{path.suffix}.json" if path.suffix else ".json"
    return path.with_suffix(suffix)


def overlay_image_format(path: Path) -> str:
    suffix = path.suffix.lower()
    if suffix in (".jpg", ".jpeg"):
        return "JPEG"
    if suffix == ".webp":
        return "WEBP"
    return "PNG"


def write_overlay_sidecar(path: Path, request: Request, regions: list[dict]) -> None:
    sidecar = {
        "schema_version": SCHEMA_VERSION,
        "output_path_redacted": "[REDACTED]",
        "dimensions": [request.image_width, request.image_height],
        "region_count": len(regions),
        "include_points": request.overlay_include_points,
        "include_labels": request.overlay_include_labels,
        "color_key": default_overlay_color_key(),
    }
    with path.open("x", encoding="utf-8") as output:
        output.write(json.dumps(sidecar, separators=(",", ":")))


def first_polygon_point(mask: dict) -> tuple[float, float] | None:
    polygons = mask.get("polygons", [])
    if not polygons or not polygons[0]:
        return None
    point = polygons[0][0]
    return (point["x"], point["y"])


def bounds(points: list[dict]) -> tuple[float, float, float, float]:
    return (min(p["x"] for p in points), min(p["y"] for p in points), max(p["x"] for p in points), max(p["y"] for p in points))


def rectangle(x1: float, y1: float, x2: float, y2: float, request: Request) -> list[dict]:
    if x2 <= x1 or y2 <= y1:
        return []
    return [
        {"x": clamp(x1, 0.0, request.image_width - 1.0), "y": clamp(y1, 0.0, request.image_height - 1.0)},
        {"x": clamp(x2, 0.0, request.image_width - 1.0), "y": clamp(y1, 0.0, request.image_height - 1.0)},
        {"x": clamp(x2, 0.0, request.image_width - 1.0), "y": clamp(y2, 0.0, request.image_height - 1.0)},
        {"x": clamp(x1, 0.0, request.image_width - 1.0), "y": clamp(y2, 0.0, request.image_height - 1.0)},
    ]


def shrink_polygon(points: list[dict], pixels: float) -> list[dict]:
    if len(points) < 3 or pixels <= 0.0:
        return points
    cx = sum(point["x"] for point in points) / len(points)
    cy = sum(point["y"] for point in points) / len(points)
    shrunk = []
    for point in points:
        dx = point["x"] - cx
        dy = point["y"] - cy
        distance = (dx * dx + dy * dy) ** 0.5
        if distance <= pixels or distance == 0.0:
            shrunk.append({"x": cx, "y": cy})
            continue
        scale = (distance - pixels) / distance
        shrunk.append({"x": cx + dx * scale, "y": cy + dy * scale})
    return shrunk


def polygon_area(points: list[dict]) -> float:
    if len(points) < 3:
        return 0.0
    area = 0.0
    previous = len(points) - 1
    for current, point in enumerate(points):
        prev = points[previous]
        area += prev["x"] * point["y"] - point["x"] * prev["y"]
        previous = current
    return abs(area) * 0.5


def ok_response(request_id: str, region_set: dict, overlay: dict | None) -> dict:
    return {"schema_version": SCHEMA_VERSION, "request_id": request_id, "status": "ok", "region_set": region_set, "overlay": overlay, "error_code": None, "warnings": region_set["warnings"]}


def error_response(request_id: str, code: str) -> dict:
    return {"schema_version": SCHEMA_VERSION, "request_id": request_id, "status": "error", "region_set": None, "overlay": None, "error_code": code, "warnings": []}


def write_response(path: Path, response: dict) -> None:
    path.write_text(json.dumps(response, separators=(",", ":")))


def clamp(value: float, low: float, high: float) -> float:
    return max(low, min(high, value))


class HelperError(Exception):
    def __init__(self, code: str, request_id: str | None = None):
        super().__init__(code)
        self.code = code
        self.request_id = request_id
