"""Drifting-model report builder mixin.

Extracts the per-object / per-segment / per-leg-direction bookkeeping
used by the drifting cascade.  These methods mutate a ``report`` dict in
place and are composed into :class:`compute.drifting_model.DriftingModelMixin`
via multiple inheritance.
"""
from __future__ import annotations

from typing import Any

from shapely.geometry import LineString, Polygon
from shapely.geometry.base import BaseGeometry

from compute.drift_corridor_geometry import (
    _extract_obstacle_segments,
    _segment_intersects_corridor,
)


class DriftingReportBuilderMixin:
    """Mixin providing the report-assembly helpers for the drifting cascade.

    The host class is expected to optionally provide
    ``self._segment_utm_to_wgs84(LineString) -> LineString`` for recording
    WGS84 segment WKT in the runtime debug layer; if absent, the
    ``segment_wkt_wgs84`` field is left as ``None``.
    """

    def _add_direct_segment_contrib(
        self,
        report: dict[str, Any],
        report_key: str,
        obstacle_key: str,
        seg_idx: int | None,
        leg_dir_key: str,
        contrib: float,
    ) -> None:
        """Write segment contribution directly, bypassing equal split helper."""
        if seg_idx is None:
            return
        obs_seg_map = report.setdefault(report_key, {}).setdefault(obstacle_key, {})
        seg_key = f"seg_{seg_idx}"
        seg_data = obs_seg_map.setdefault(seg_key, {})
        seg_data[leg_dir_key] = seg_data.get(leg_dir_key, 0.0) + contrib

    def _record_edge_meta(
        self,
        *,
        report: dict[str, Any],
        obs_key: str,
        seg_idx: int | None,
        leg_dir_key: str,
        obs_type: str,
        hole_pct: float,
        h_eff: float,
        reachable_width: float,
        edge_h_eff: float,
        len_frac: float,
        edge_dist: float,
        edge_p_nr: float,
        edge_hole: float,
        base: float,
        rp: float,
        contrib: float,
    ) -> None:
        """Store per-edge calculation trace for backtracing.

        For a given ``(obstacle, edge, leg_direction)`` the *static* inputs
        (``hole_pct``, ``h_eff``, ``reachable_width``, ``edge_h_eff``,
        ``len_frac``, ``edge_dist``, ``edge_p_nr``, ``edge_hole``) are
        fixed -- only ``base`` and the number of ship cells contributing
        vary.  Users back-trace the contribution via
        ``contrib = base * rp * (h_eff * len_frac) * edge_p_nr``
        where
        ``len_frac = reachable_width / Σ reachable_widths`` (1D shadow-carved
        share of the polygon) and ``edge_h_eff = reachable_width /
        leg_drift_width`` is the pure-geometric per-edge share (kept for
        backtracing only -- multiplying by it directly would ignore the
        AIS ship-density profile embedded in the analytical h_eff).
        """
        if seg_idx is None:
            return
        meta_root = report.setdefault('edge_calc_meta', {})
        obs_map = meta_root.setdefault(obs_key, {})
        seg_key = f"seg_{seg_idx}"
        seg_map = obs_map.setdefault(seg_key, {})
        entry = seg_map.setdefault(leg_dir_key, {
            'obs_type': obs_type,
            'hole_pct': float(hole_pct),
            'h_eff': float(h_eff),
            'reachable_width_m': float(reachable_width),
            'edge_h_eff': float(edge_h_eff),
            'len_frac': float(len_frac),
            'edge_dist_m': float(edge_dist),
            'edge_p_nr': float(edge_p_nr),
            'edge_hole': float(edge_hole),
            'exposure_sum': 0.0,   # sum of (base * rp) across ship cells
            'contrib_sum': 0.0,    # sum of contributions across ship cells
            'cell_count': 0,
        })
        # h_eff / edge_hole may vary marginally across cells if anchor logic
        # differs; refresh so the stored value reflects the last observed one.
        entry['h_eff'] = float(h_eff)
        entry['edge_hole'] = float(edge_hole)
        entry['exposure_sum'] += float(base) * float(rp)
        entry['contrib_sum'] += float(contrib)
        entry['cell_count'] += 1

    def _accumulate_object_prob(
        self,
        report: dict[str, Any],
        event: str,
        contrib: float,
        idx: int,
        structures: list[dict[str, Any]],
        depths: list[dict[str, Any]],
    ) -> None:
        try:
            if idx is None:
                return
            if event == 'allision':
                o = structures[idx]
                okey = f"Structure - {o.get('id', str(idx))}"
            elif event in ('grounding', 'anchoring'):
                d = depths[idx]
                okey = f"Depth - {d.get('id', str(idx))}"
            else:
                return
            ob = report.setdefault('by_object', {}).setdefault(
                okey, {'allision': 0.0, 'grounding': 0.0, 'anchoring': 0.0},
            )
            ob[event] = ob.get(event, 0.0) + contrib
        except Exception:  # nosec B110 B112
            pass

    def _accumulate_leg_dir_stats(
        self,
        report: dict[str, Any],
        event: str,
        contrib: float,
        dist: float,
        base: float,
        rp: float,
        leg_dir_key: str,
        anchor_factor: float,
        p_nr: float,
        ov_frac: float,
        freq: float,
        ship_type: int,
        ship_size: int,
    ) -> None:
        rec = report['by_leg_direction'].setdefault(leg_dir_key, {
            'base_hours': 0.0, 'contrib_allision': 0.0, 'contrib_grounding': 0.0,
            'ship_categories': {}, 'min_distance_allision': None,
            'min_distance_grounding': None, 'anchor_factor_sum': 0.0,
            'not_repaired_sum': 0.0, 'overlap_sum': 0.0, 'weight_sum': 0.0,
        })
        rec['base_hours'] += base * rp
        if event == 'allision':
            rec['contrib_allision'] += contrib
            md = rec['min_distance_allision']
            rec['min_distance_allision'] = dist if md is None or dist < md else md
        else:
            rec['contrib_grounding'] += contrib
            md = rec['min_distance_grounding']
            rec['min_distance_grounding'] = dist if md is None or dist < md else md
        w = base * rp
        rec['anchor_factor_sum'] += anchor_factor * w
        rec['not_repaired_sum'] += p_nr * w
        rec['overlap_sum'] += ov_frac * w
        rec['weight_sum'] += w
        cat_key = f"{ship_type}-{ship_size}"
        scat = rec['ship_categories'].setdefault(cat_key, {'allision': 0.0, 'grounding': 0.0, 'freq': 0.0})
        scat[event] += contrib
        scat['freq'] = freq

    def _accumulate_structure_legdir(
        self,
        report: dict[str, Any],
        event: str,
        idx: int,
        structures: list[dict[str, Any]],
        leg_dir_key: str,
        contrib: float,
        d_idx: int,
        drift_corridor: 'Polygon | None',
        leg: 'LineString | None',
    ) -> None:
        try:
            if event != 'allision' or idx is None:
                return
            s = structures[idx]
            skey = f"Structure - {s.get('id', str(idx))}"
            s_map = report['by_structure_legdir'].setdefault(skey, {})
            s_map[leg_dir_key] = s_map.get(leg_dir_key, 0.0) + contrib
            if drift_corridor is not None and s.get('wkt') is not None:
                math_drift_angle = (90 - d_idx * 45) % 360
                self._update_segment_contributions(
                    report, 'by_structure_segment_legdir', skey, leg_dir_key,
                    contrib, s['wkt'], drift_corridor, math_drift_angle, leg,
                )
        except Exception:  # nosec B110 B112
            pass

    def _accumulate_depth_legdir(
        self,
        report: dict[str, Any],
        event: str,
        idx: int,
        depths: list[dict[str, Any]],
        leg_dir_key: str,
        contrib: float,
        d_idx: int,
        drift_corridor: 'Polygon | None',
        leg: 'LineString | None',
    ) -> None:
        try:
            if event != 'grounding' or idx is None:
                return
            d = depths[idx]
            dkey = f"Depth - {d.get('id', str(idx))}"
            d_map = report['by_depth_legdir'].setdefault(dkey, {})
            d_map[leg_dir_key] = d_map.get(leg_dir_key, 0.0) + contrib
            if drift_corridor is not None and d.get('wkt') is not None:
                math_drift_angle = (90 - d_idx * 45) % 360
                self._update_segment_contributions(
                    report, 'by_depth_segment_legdir', dkey, leg_dir_key,
                    contrib, d['wkt'], drift_corridor, math_drift_angle, leg,
                )
        except Exception:  # nosec B110 B112
            pass

    def _update_report(self,
                       report: dict[str, Any],
                       event: str,
                       contrib: float,
                       idx: int,
                       structures: list[dict[str, Any]],
                       depths: list[dict[str, Any]],
                       seg_id: str,
                       cell: dict[str, float],
                       d_idx: int,
                       dist: float,
                       base: float,
                       rp: float,
                       anchor_factor: float,
                       p_nr: float,
                       ov_frac: float,
                       freq: float,
                       ship_type: int,
                       ship_size: int,
                       drift_corridor: Polygon | None = None,
                       leg: LineString | None = None,
                       ) -> None:
        """Update report dictionaries with contribution."""
        leg_dir_label = str(cell.get('direction', '')).strip()
        leg_dir_key = f"{seg_id}:{leg_dir_label}:{d_idx * 45}"
        self._accumulate_object_prob(report, event, contrib, idx, structures, depths)
        self._accumulate_leg_dir_stats(
            report, event, contrib, dist, base, rp, leg_dir_key,
            anchor_factor, p_nr, ov_frac, freq, ship_type, ship_size,
        )
        self._accumulate_structure_legdir(
            report, event, idx, structures, leg_dir_key, contrib, d_idx, drift_corridor, leg
        )
        self._accumulate_depth_legdir(
            report, event, idx, depths, leg_dir_key, contrib, d_idx, drift_corridor, leg
        )

    def _record_seg_debug_meta(
        self,
        report: dict[str, Any],
        obstacle_key: str,
        seg_key: str,
        leg_dir_key: str,
        contrib_seg: float,
        seg_line: 'LineString',
        drift_corridor: 'Polygon',
        leg: 'LineString | None',
    ) -> None:
        try:
            inter_len = 0.0
            try:
                inter_len = float(seg_line.intersection(drift_corridor).length)
            except Exception:  # nosec B110 B112
                pass
            dist_to_leg = None
            if leg is not None:
                try:
                    dist_to_leg = float(seg_line.distance(leg))
                except Exception:  # nosec B110 B112
                    pass
            runtime_map = report.setdefault('runtime_segment_hits', {}).setdefault(obstacle_key, {})
            seg_meta = runtime_map.setdefault(seg_key, {
                'segment_wkt_utm': seg_line.wkt, 'segment_wkt_wgs84': None, 'hits': {},
            })
            if seg_meta.get('segment_wkt_wgs84') is None:
                conv = getattr(self, '_segment_utm_to_wgs84', None)
                if conv is not None:
                    try:
                        seg_meta['segment_wkt_wgs84'] = conv(seg_line).wkt
                    except Exception:  # nosec B110 B112
                        pass
            hit = seg_meta['hits'].setdefault(leg_dir_key, {
                'count': 0, 'max_intersection_len_m': 0.0, 'min_distance_to_leg_m': None, 'contrib_sum': 0.0,
            })
            hit['count'] += 1
            hit['contrib_sum'] += float(contrib_seg)
            if inter_len > float(hit.get('max_intersection_len_m', 0.0)):
                hit['max_intersection_len_m'] = inter_len
            if dist_to_leg is not None:
                cur = hit.get('min_distance_to_leg_m')
                if cur is None or dist_to_leg < float(cur):
                    hit['min_distance_to_leg_m'] = dist_to_leg
        except Exception:  # nosec B110 B112
            pass

    def _update_segment_contributions(
        self,
        report: dict[str, Any],
        report_key: str,
        obstacle_key: str,
        leg_dir_key: str,
        contrib: float,
        obs_geom: BaseGeometry,
        drift_corridor: Polygon,
        drift_angle: float | None = None,
        leg: LineString | None = None,
    ) -> None:
        """
        Track which segments of an obstacle are hit by a drift corridor.

        Distributes the contribution among segments that actually intersect
        with the drift corridor AND are in the drift direction from the leg.

        Args:
            report: The report dictionary to update
            report_key: 'by_structure_segment_legdir' or 'by_depth_segment_legdir'
            obstacle_key: Key for the obstacle (e.g., "Structure - id")
            leg_dir_key: Key for leg-direction (e.g., "1:North:0")
            contrib: Total contribution for this obstacle from this leg-direction
            obs_geom: Obstacle geometry (UTM)
            drift_corridor: Drift corridor polygon (UTM)
            drift_angle: Drift direction in degrees (math convention: 0=East, 90=North)
            leg: The traffic leg LineString for direction checking
        """
        try:
            segments = _extract_obstacle_segments(obs_geom)
            if not segments:
                return
            leg_centroid = None
            if leg is not None:
                centroid = leg.centroid
                leg_centroid = (centroid.x, centroid.y)
            intersecting_indices: list[int] = []
            seg_intersection_len: dict[int, float] = {}
            for seg_idx, segment in enumerate(segments):
                if _segment_intersects_corridor(segment, drift_corridor, drift_angle, leg_centroid):
                    intersecting_indices.append(seg_idx)
                    try:
                        seg_intersection_len[seg_idx] = float(
                            LineString([segment[0], segment[1]]).intersection(drift_corridor).length
                        )
                    except Exception:  # nosec B110 B112
                        seg_intersection_len[seg_idx] = 0.0
            if not intersecting_indices:
                return
            total_inter_len = sum(max(seg_intersection_len.get(i, 0.0), 0.0) for i in intersecting_indices)
            obs_seg_map = report.setdefault(report_key, {}).setdefault(obstacle_key, {})
            for seg_idx in intersecting_indices:
                if total_inter_len > 0.0:
                    contrib_seg = contrib * max(seg_intersection_len.get(seg_idx, 0.0), 0.0) / total_inter_len
                else:
                    contrib_seg = contrib / len(intersecting_indices)
                seg_key = f"seg_{seg_idx}"
                seg_data = obs_seg_map.setdefault(seg_key, {})
                seg_data[leg_dir_key] = seg_data.get(leg_dir_key, 0.0) + contrib_seg
                seg_line = LineString([segments[seg_idx][0], segments[seg_idx][1]])
                self._record_seg_debug_meta(
                    report, obstacle_key, seg_key, leg_dir_key, contrib_seg, seg_line, drift_corridor, leg,
                )
        except Exception:  # nosec B110 B112
            pass

    def _update_anchoring_report(
        self,
        report: dict[str, Any],
        anchor_contrib: float,
        obs_idx: int,
        depths: list[dict[str, Any]],
        seg_id: str,
        d_idx: int,
        dist: float,
        hole_pct: float,
        drift_corridor: Polygon | None,
        leg: LineString,
    ) -> None:
        """
        Track anchoring contributions per depth and per segment.

        Anchoring is now tracked like grounding and allision - we record which
        segments of a depth obstacle would receive the anchoring shadow.

        Args:
            report: The report dictionary to update
            anchor_contrib: Anchoring contribution value
            obs_idx: Index of the depth obstacle
            depths: List of depth dictionaries
            seg_id: Segment (leg) ID
            d_idx: Direction index (compass convention: 0=N, 1=NE, ...)
            dist: Distance to obstacle
            hole_pct: Probability hole percentage
            drift_corridor: Drift corridor polygon (UTM)
            leg: The traffic leg LineString
        """
        try:
            # Direction names for reporting
            dir_names = ['N', 'NE', 'E', 'SE', 'S', 'SW', 'W', 'NW']
            dir_name = dir_names[d_idx % 8]
            compass_angle = d_idx * 45
            leg_dir_key = f"{seg_id}:{dir_name}:{compass_angle}"

            # Per-depth per leg-direction accumulation
            d = depths[obs_idx]
            dkey = f"Anchoring - {d.get('id', str(obs_idx))}"
            d_map = report['by_anchoring_legdir'].setdefault(dkey, {})
            d_map[leg_dir_key] = d_map.get(leg_dir_key, 0.0) + anchor_contrib

            # Per-segment tracking for anchoring
            if drift_corridor is not None:
                obs_geom = d.get('wkt')
                if obs_geom is not None:
                    # Convert compass angle to math convention
                    math_drift_angle = (90 - compass_angle) % 360
                    self._update_segment_contributions(
                        report, 'by_anchoring_segment_legdir',
                        dkey, leg_dir_key, anchor_contrib, obs_geom, drift_corridor,
                        math_drift_angle, leg
                    )
        except Exception:  # nosec B110 B112
            pass
