"""Aspect-correct COCO-17 features, without bbox geometry or raw-pose mutation."""
from dataclasses import dataclass
import math

from app.contracts.frames import PoseObservation
from app.fall_config import FallConfig
from app.pose_config import POSE_MODEL

# Each signature is an exact joint set; ankle and knee spans never substitute.
SPAN_SOURCES = (
    ('mid_ankle', (5, 6), (15, 16)),
    ('left_ankle', (5,), (15,)),
    ('right_ankle', (6,), (16,)),
    ('mid_knee', (5, 6), (13, 14)),
    ('left_knee', (5,), (13,)),
    ('right_knee', (6,), (14,)),
)
EPS = 1e-9


@dataclass(frozen=True)
class PoseFeatures:
    torso_source: str | None = None
    torso_angle: float | None = None
    hip_source: str | None = None
    hip_y: float | None = None
    spans: tuple[tuple[str, float], ...] = ()
    knee_angles: tuple[float, ...] = ()
    knee_guard: bool | None = None

    @property
    def span_source(self):
        return self.spans[0][0] if self.spans else None

    @property
    def span(self):
        return self.spans[0][1] if self.spans else None


def extract_features(pose: PoseObservation, config: FallConfig) -> PoseFeatures:
    if not isinstance(pose, PoseObservation) or pose.model_key != POSE_MODEL:
        raise ValueError('FALL requires canonical RFDETRKeypointPreview PoseObservation.')
    if pose.score < config.person_score:
        return PoseFeatures()
    width, height = pose.frame.frame_width, pose.frame.frame_height
    # Raw off-screen coordinates stay unchanged: canonical Pose permits them.
    points = tuple((x * width, y * height) if score >= config.joint_score
                   and all(math.isfinite(v) for v in (x, y, score)) else None
                   for x, y, score in pose.keypoints)

    def mean(indices):
        values = [points[i] for i in indices]
        if any(p is None for p in values):
            return None
        return tuple(sum(p[axis] for p in values) / len(values) for axis in (0, 1))

    torso_source = torso_angle = None
    for source, shoulders, hips in (('mid', (5, 6), (11, 12)),
                                    ('left', (5,), (11,)), ('right', (6,), (12,))):
        top, bottom = mean(shoulders), mean(hips)
        if top is not None and bottom is not None:
            dx, dy = bottom[0] - top[0], bottom[1] - top[1]
            if math.hypot(dx, dy) > EPS:
                torso_source = source
                torso_angle = math.degrees(math.atan2(abs(dx), abs(dy)))
                break
    hip_source = hip_y = None
    for source, indices in (('mid', (11, 12)), ('left', (11,)), ('right', (12,))):
        hip = mean(indices)
        if hip is not None:
            hip_source, hip_y = source, hip[1]
            break
    spans = []
    for signature, upper, lower in SPAN_SOURCES:
        a, b = mean(upper), mean(lower)
        if a is not None and b is not None:
            spans.append((signature, abs(b[1] - a[1])))
    knees = []
    for hip, knee, ankle in ((11, 13, 15), (12, 14, 16)):
        a, b, c = points[hip], points[knee], points[ankle]
        if a is None or b is None or c is None:
            continue
        u, v = (a[0] - b[0], a[1] - b[1]), (c[0] - b[0], c[1] - b[1])
        length = math.hypot(*u) * math.hypot(*v)
        if length > EPS:
            cosine = max(-1., min(1., (u[0] * v[0] + u[1] * v[1]) / length))
            knees.append(math.degrees(math.acos(cosine)))
    guard = None
    if torso_angle is not None:
        if torso_angle >= config.knee_guard_torso_max_deg:
            guard = False
        elif knees:
            guard = any(angle <= config.knee_guard_angle_deg for angle in knees)
    return PoseFeatures(torso_source, torso_angle, hip_source, hip_y,
                        tuple(spans), tuple(knees), guard)
