"""
VPandit Palmistry — Reference external CV/ML microservice
=========================================================

REFERENCE IMPLEMENTATION. This is the optional external scorer that the VPandit
Laravel app calls when an admin configures Palmistry -> CV Service (endpoint +
bearer key). It implements the exact request/response contract the app expects,
extended with mount segmentation and special-sign detection.

The Laravel proxy (PalmistryController::cv) POSTs:
    {
      "left_image":  "data:image/...;base64,..."  | null,
      "right_image": "data:image/...;base64,..."  | null,
      "dominant_hand": "LEFT" | "RIGHT",
      "lines": ["life_line","head_line","heart_line","fate_line","sun_line"]
    }

and expects back:
    {
      "scores":   [{hand_side, line_key, presence, depth, continuity, vector}],
      "features": [{hand_side, feature_key, mount_key, line_key, confidence, box}],
      "mounts":   [{hand_side, mount_key, box}]          # optional
    }

`vector` is a polyline of [x,y] keypoints (pixels or 0..1 fractions — the app
normalises either). `box` is [x_min,y_min,x_max,y_max] normalised 0..1.

Pipeline:
    1. MediaPipe Hands -> 21 landmarks -> dynamic mount ROI masks.
    2. Line ROI sampling (Frangi/Sobel ridge response) -> presence/depth/continuity
       + a traced polyline per line.
    3. Pattern recognition on mounts & lines -> STAR / CROSS / TRIANGLE / SQUARE /
       ISLAND / GRILLE / MOLE_SPOT via contour + intersection heuristics.

Run:
    pip install fastapi uvicorn mediapipe opencv-python numpy scikit-image pillow
    uvicorn cv_service:app --host 0.0.0.0 --port 8099
Then set the app's CV endpoint to  http://<host>:8099/score  and a bearer key.
"""
from __future__ import annotations

import base64
import io
from typing import Optional

import numpy as np
from fastapi import FastAPI, Header, HTTPException
from pydantic import BaseModel

try:
    import cv2
    import mediapipe as mp
    from PIL import Image
    from skimage.filters import frangi
    _DEPS = True
except Exception:  # keep the module importable even without the CV stack
    _DEPS = False

app = FastAPI(title="VPandit Palmistry CV Service", version="1.0")

# Optional shared-secret; set to match the key configured in the app.
API_KEY: Optional[str] = None

DEFAULT_LINES = ["life_line", "head_line", "heart_line", "fate_line", "sun_line"]

# MediaPipe landmark indices.
WRIST = 0
INDEX_MCP, MIDDLE_MCP, RING_MCP, PINKY_MCP = 5, 9, 13, 17
THUMB_CMC, THUMB_TIP = 1, 4


class ScoreRequest(BaseModel):
    left_image: Optional[str] = None
    right_image: Optional[str] = None
    dominant_hand: str = "RIGHT"
    lines: Optional[list[str]] = None


# ── image helpers ────────────────────────────────────────────────────────────

def _decode(data_url: Optional[str]) -> Optional["np.ndarray"]:
    if not data_url:
        return None
    b64 = data_url.split(",", 1)[1] if "," in data_url else data_url
    raw = base64.b64decode(b64)
    img = Image.open(io.BytesIO(raw)).convert("RGB")
    return np.array(img)


def _landmarks(rgb: "np.ndarray") -> Optional["np.ndarray"]:
    """Return 21×2 normalised (0..1) hand landmarks, or None."""
    with mp.solutions.hands.Hands(static_image_mode=True, max_num_hands=1,
                                  min_detection_confidence=0.4) as hands:
        res = hands.process(rgb)
    if not res.multi_hand_landmarks:
        return None
    lm = res.multi_hand_landmarks[0].landmark
    return np.array([[p.x, p.y] for p in lm], dtype=np.float32)


# ── mount segmentation (landmark-relative ROI centres) ───────────────────────

def mount_boxes(lm: "np.ndarray") -> dict[str, list[float]]:
    """Normalised [x0,y0,x1,y1] boxes per mount, derived from landmarks."""
    def box(cx: float, cy: float, r: float = 0.08) -> list[float]:
        return [max(0, cx - r), max(0, cy - r), min(1, cx + r), min(1, cy + r)]

    idx, mid, ring, pky = lm[INDEX_MCP], lm[MIDDLE_MCP], lm[RING_MCP], lm[PINKY_MCP]
    wrist, thumb = lm[WRIST], lm[THUMB_CMC]
    palm_cx, palm_cy = float(np.mean(lm[:, 0])), float(np.mean(lm[:, 1]))
    return {
        "JUPITER":    box(idx[0],  idx[1] + 0.03),
        "SATURN":     box(mid[0],  mid[1] + 0.03),
        "SUN_APOLLO": box(ring[0], ring[1] + 0.03),
        "MERCURY":    box(pky[0],  pky[1] + 0.03),
        "VENUS":      box((thumb[0] + wrist[0]) / 2, (thumb[1] + wrist[1]) / 2, 0.10),
        "MOON_LUNA":  box(pky[0] * 0.9 + wrist[0] * 0.1, wrist[1] * 0.65 + palm_cy * 0.35, 0.10),
        "MARS_UPPER": box(pky[0], palm_cy, 0.07),
        "MARS_LOWER": box(thumb[0], palm_cy, 0.07),
        "RAHU":       box(palm_cx, palm_cy, 0.09),
        "KETU":       box(palm_cx, wrist[1] * 0.85 + palm_cy * 0.15, 0.08),
    }


# ── line scoring (ridge response along canonical ROI) ────────────────────────

# Canonical ROI polylines as fractions of the hand bounding box.
LINE_ROI = {
    "heart_line": [[0.15, 0.28], [0.55, 0.235], [0.90, 0.27]],
    "head_line":  [[0.15, 0.46], [0.55, 0.44], [0.85, 0.47]],
    "life_line":  [[0.32, 0.31], [0.22, 0.58], [0.35, 0.84]],
    "fate_line":  [[0.52, 0.88], [0.515, 0.52], [0.52, 0.36]],
    "sun_line":   [[0.72, 0.60], [0.72, 0.40], [0.72, 0.32]],
}


def _ridge(gray: "np.ndarray") -> "np.ndarray":
    """Frangi vesselness — highlights thin dark line creases."""
    inv = 1.0 - (gray.astype(np.float32) / 255.0)
    return frangi(inv, sigmas=range(1, 4), black_ridges=False)


def score_hand(rgb: "np.ndarray", lines: list[str]) -> tuple[list[dict], dict[str, list[float]], list[dict]]:
    lm = _landmarks(rgb)
    h, w = rgb.shape[:2]
    gray = cv2.cvtColor(rgb, cv2.COLOR_RGB2GRAY)
    gray = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8)).apply(gray)
    ridge = _ridge(gray)

    scores, features = [], []
    for key in lines:
        roi = LINE_ROI.get(key, [[0.2, 0.5], [0.8, 0.5]])
        pts = _sample(roi, 64)
        vals = []
        vector = []
        for fx, fy in pts:
            x, y = int(fx * w), int(fy * h)
            win = ridge[max(0, y - 6):y + 6, max(0, x - 6):x + 6]
            vals.append(float(win.max()) if win.size else 0.0)
            vector.append([round(fx, 4), round(fy, 4)])
        vals = np.array(vals)
        above = vals > 0.15
        presence = float(above.mean())
        depth = float(min(1.0, vals.mean() * 3.0))
        continuity = float(_longest_run(above) / len(above))
        scores.append({
            "line_key": key,
            "presence": round(presence, 3),
            "depth": round(depth, 3),
            "continuity": round(continuity, 3),
            "vector": vector,
        })
        # Island / bar detection on the line (very light heuristic).
        features += _line_signs(key, ridge, pts, w, h)

    mounts = mount_boxes(lm) if lm is not None else {}
    features += _mount_signs(ridge, mounts, w, h)
    return scores, mounts, features


def _sample(poly: list[list[float]], n: int) -> list[tuple[float, float]]:
    poly = np.array(poly, dtype=np.float32)
    seg = np.linalg.norm(np.diff(poly, axis=0), axis=1)
    total = float(seg.sum()) or 1.0
    out = []
    for k in range(n):
        t = total * k / (n - 1)
        i = 0
        while i < len(seg) - 1 and t > seg[i]:
            t -= seg[i]
            i += 1
        f = t / seg[i] if seg[i] else 0.0
        out.append((float(poly[i][0] + (poly[i + 1][0] - poly[i][0]) * f),
                    float(poly[i][1] + (poly[i + 1][1] - poly[i][1]) * f)))
    return out


def _longest_run(mask: "np.ndarray") -> int:
    best = run = 0
    for a in mask:
        run = run + 1 if a else 0
        best = max(best, run)
    return best


def _line_signs(key: str, ridge: "np.ndarray", pts, w: int, h: int) -> list[dict]:
    """Detect an ISLAND (a gap-then-return) or BAR crossing a line ROI."""
    out = []
    vals = np.array([ridge[min(h - 1, int(fy * h)), min(w - 1, int(fx * w))] for fx, fy in pts])
    strong = vals > 0.15
    # An island reads as strong-weak-strong along a short span.
    for i in range(4, len(strong) - 4):
        if strong[i - 3] and not strong[i] and strong[i + 3]:
            fx, fy = pts[i]
            out.append({"feature_key": "ISLAND", "line_key": key, "mount_key": None,
                        "confidence": 0.55, "box": {"x": round(fx, 4), "y": round(fy, 4)}})
            break
    return out


def _mount_signs(ridge: "np.ndarray", mounts: dict, w: int, h: int) -> list[dict]:
    """Detect STAR / CROSS / GRILLE style clusters inside each mount ROI."""
    out = []
    for mk, (x0, y0, x1, y1) in mounts.items():
        sub = ridge[int(y0 * h):int(y1 * h), int(x0 * w):int(x1 * w)]
        if sub.size == 0:
            continue
        density = float((sub > 0.2).mean())
        cx, cy = (x0 + x1) / 2, (y0 + y1) / 2
        if density > 0.28:
            out.append({"feature_key": "GRILLE", "mount_key": mk, "line_key": None,
                        "confidence": round(min(0.9, density + 0.3), 2),
                        "box": {"x": round(cx, 4), "y": round(cy, 4)}})
    return out


# ── endpoint ─────────────────────────────────────────────────────────────────

@app.post("/score")
def score(req: ScoreRequest, authorization: Optional[str] = Header(default=None)):
    if API_KEY and authorization != f"Bearer {API_KEY}":
        raise HTTPException(status_code=401, detail="unauthorized")
    if not _DEPS:
        raise HTTPException(status_code=503, detail="CV dependencies not installed on this host")

    lines = req.lines or DEFAULT_LINES
    all_scores, all_features, all_mounts = [], [], []
    for hand, data_url in (("LEFT", req.left_image), ("RIGHT", req.right_image)):
        rgb = _decode(data_url)
        if rgb is None:
            continue
        try:
            s, mounts, feats = score_hand(rgb, lines)
        except Exception:
            continue
        for row in s:
            row["hand_side"] = hand
        for f in feats:
            f["hand_side"] = hand
        for mk, box in mounts.items():
            all_mounts.append({"hand_side": hand, "mount_key": mk, "box": box})
        all_scores += s
        all_features += feats

    return {"scores": all_scores, "features": all_features, "mounts": all_mounts}


@app.get("/health")
def health():
    return {"ok": True, "deps": _DEPS}
