import json
import math
from math import atan2, cos, pi, sin

from topaze.file_utils import FileUtils
from topaze.toolbelt.i18n import tr


def v0_is_computable_map_from_dict(
    obs_data: dict,
    *,
    min_refs: int = 1,
    require_ah: bool = True,
    require_known_xy: bool = True,
    verbose: bool = True,
) -> dict:
    """
    Return {station_id: True/False} telling if V0 is computable for each station.

    A station's V0 is deemed computable if there are at least `min_refs`
    usable sights originating from that station to targets with known XY,
    and (optionally) each sight has a horizontal angle (AH).

    Parameters:
      - min_refs: minimal usable references required (default 1)
      - require_ah: if True, AH must be present on a sight
      - require_known_xy: if True, target must have known X and Y
      - verbose: if True, prints a short summary

    This function is agnostic to V0 values (your JSON need not contain them).
    """
    stations = obs_data.get("stations", []) or []
    obs_list = obs_data.get("obs", []) or []

    # Index stations and XY availability
    by_id = {
        st.get("matricule"): st for st in stations if st.get("matricule") is not None
    }

    def has_xy(pid: str) -> bool:
        st = by_id.get(pid)
        return st is not None and st.get("x") is not None and st.get("y") is not None

    # Count usable references per station
    counts = {sid: 0 for sid in by_id.keys()}
    seen_pair = set()  # to avoid counting the same (station,target) twice

    for o in obs_list:
        sid = o.get("origine")
        tid = o.get("cible")
        if sid is None or tid is None or sid not in by_id or tid not in by_id:
            continue
        if require_ah and (o.get("ah", None) is None):
            continue
        if require_known_xy and not has_xy(tid):
            continue

        key = (sid, tid)
        if key not in seen_pair:
            counts[sid] = counts.get(sid, 0) + 1
            seen_pair.add(key)

    # Build boolean map
    computable = {sid: (counts.get(sid, 0) >= int(min_refs)) for sid in by_id.keys()}

    if verbose:
        print(tr("V0 computability summary"))
        for sid in by_id.keys():
            if computable[sid]:
                print(
                    tr("Station {st}: V0 computable (n={n})").format(
                        st=sid, n=counts.get(sid, 0)
                    )
                )
            else:
                print(
                    tr("Station {st}: V0 NOT computable (n={n})").format(
                        st=sid, n=counts.get(sid, 0)
                    )
                )

    return computable


# Convenience loaders (pick the one most useful):
def v0_is_computable_map_from_json_path(json_path: str, **kwargs) -> dict:
    with open(json_path, "r", encoding="utf-8") as f:
        obs_data = json.load(f)
    return v0_is_computable_map_from_dict(obs_data, **kwargs)


def v0_is_computable_map_from_tempfile(
    tempfile_name: str = "calculable_v0.json", **kwargs
) -> dict:
    data = FileUtils.load_temp_file(tempfile_name)
    if not data:
        print(tr("No data found in temp file {name}.").format(name=tempfile_name))
        return {}
    obs_data = json.loads(data)
    return v0_is_computable_map_from_dict(obs_data, **kwargs)


"""
V0 calculable for stations ?
# From temp file (like your other workflows)
result = v0_is_computable_map_from_tempfile("calculable_v0.json")

# From a file path
# result = v0_is_computable_map_from_json_path("/path/to/calculable_v0.json")

# If we want to relax constraints (e.g., accept sights even if AH missing)
# result = v0_is_computable_map_from_tempfile("calculable_v0.json", require_ah=False)
"""


def wrap_gr_0_400(g: float) -> float:
    return g % 400.0


def wrap_gr_m200_200(g: float) -> float:
    # Wrap grads into (-200, 200]
    return (g + 200.0) % 400.0 - 200.0


def az_gr_north_cw(dx: float, dy: float) -> float:
    """Azimuth in grads, 0g = North, clockwise positive."""
    return wrap_gr_0_400(atan2(dx, dy) * 200.0 / pi)


def angle_interne_gr(dg):
    """Return the internal angle in grads (<= 200g) from a signed diff."""
    dg = abs(wrap_gr_0_400(dg))
    return 400.0 - dg if dg > 200.0 else dg


def line_intersection(P, azr, Q, bzr):
    """
    Intersection of two rays:
      P + t * u, with u = (sin(azr), cos(azr))
      Q + s * v, with v = (sin(bzr), cos(bzr))
    Returns (x,y) or None if nearly parallel.
    """
    ux, uy = sin(azr), cos(azr)
    vx, vy = sin(bzr), cos(bzr)
    det = ux * (-vy) - uy * (-vx)  # determinant of [u | -v]
    if abs(det) < 1e-12:
        return None
    # Solve: P + t u = Q + s v  ->  t u - s v = Q - P
    rhsx, rhsy = Q[0] - P[0], Q[1] - P[1]
    t = (rhsx * (-vy) - rhsy * (-vx)) / det
    x = P[0] + t * ux
    y = P[1] + t * uy
    return (x, y)
