"""쌍끌이 트롤 공조 탐지 — DAR-03 G-06.

두 선박의 AIS 궤적을 분석하여 쌍끌이 조업 여부를 판정한다.
판정 기준 (DAR-03 / Kroodsma 2018):
- 선박 간격 ≤ 500m (0.27 NM)
- 속력 차이 ≤ 0.5 kn
- 방향 차이 ≤ 10°
- 조업 속력 2.0~4.0 kn
- 지속 시간 ≥ 2시간
- 동시 AIS 차단 ≥ 30분 → P-01 추가
"""

from __future__ import annotations

import logging
import math
from typing import Optional

import pandas as pd

try:
    from algorithms.location import haversine_nm
except ImportError:
    def haversine_nm(lat1: float, lon1: float, lat2: float, lon2: float) -> float:  # type: ignore[misc]
        """두 좌표 간 거리 (해리). fallback 구현."""
        R = 3440.065
        dlat = math.radians(lat2 - lat1)
        dlon = math.radians(lon2 - lon1)
        a = (math.sin(dlat / 2) ** 2
             + math.cos(math.radians(lat1)) * math.cos(math.radians(lat2))
             * math.sin(dlon / 2) ** 2)
        return 2 * R * math.asin(math.sqrt(a))

logger = logging.getLogger(__name__)

# ──────────────────────────────────────────────────────────────
# 상수
# ──────────────────────────────────────────────────────────────

PROXIMITY_NM = 0.27        # 500m ≈ 0.27 NM
SOG_DELTA_MAX = 0.5        # kn
COG_DELTA_MAX = 10.0       # degrees
SOG_MIN = 2.0              # kn (조업 속력 하한)
SOG_MAX = 4.0              # kn (조업 속력 상한)
MIN_SYNC_CYCLES = 24       # 24 × 5min = 2시간
SIMULTANEOUS_GAP_MIN = 30  # 동시 AIS 차단 기준 (분)
CYCLE_INTERVAL_MIN = 5     # 5분 리샘플 데이터

# scan_unregistered_pairs 전용
CELL_SIZE = 0.01           # ~1.1km 격자
CANDIDATE_PROXIMITY_FACTOR = 2.0   # 후보 탐색 반경: PROXIMITY_NM × 2
CANDIDATE_SOG_MIN = 1.5    # 후보 속력 하한 (완화)
CANDIDATE_SOG_MAX = 5.0    # 후보 속력 상한 (완화)

# ──────────────────────────────────────────────────────────────
# 내부 헬퍼
# ──────────────────────────────────────────────────────────────

def _cog_delta(cog_a: float, cog_b: float) -> float:
    """두 COG 값의 각도 차이 (0~180°)."""
    return abs((cog_a - cog_b + 180.0) % 360.0 - 180.0)


def _find_gaps(df: pd.DataFrame, gap_threshold_min: float = 10.0) -> list[tuple[pd.Timestamp, pd.Timestamp]]:
    """DataFrame 행 간 gap_threshold_min 초과 gap 구간 목록 반환.

    Returns:
        list of (gap_start, gap_end) as pd.Timestamp pairs
    """
    if len(df) < 2:
        return []
    ts_series = pd.to_datetime(df['timestamp']).sort_values().reset_index(drop=True)
    gaps: list[tuple[pd.Timestamp, pd.Timestamp]] = []
    for i in range(1, len(ts_series)):
        delta_min = (ts_series.iloc[i] - ts_series.iloc[i - 1]).total_seconds() / 60.0
        if delta_min > gap_threshold_min:
            gaps.append((ts_series.iloc[i - 1], ts_series.iloc[i]))
    return gaps


def _overlap_minutes(
    gaps_a: list[tuple[pd.Timestamp, pd.Timestamp]],
    gaps_b: list[tuple[pd.Timestamp, pd.Timestamp]],
) -> float:
    """두 gap 목록의 시간 구간 겹침 합계 (분)."""
    total = 0.0
    for start_a, end_a in gaps_a:
        for start_b, end_b in gaps_b:
            overlap_start = max(start_a, start_b)
            overlap_end = min(end_a, end_b)
            if overlap_end > overlap_start:
                total += (overlap_end - overlap_start).total_seconds() / 60.0
    return total


def _max_sync_block(synced_series: 'pd.Series[bool]') -> int:
    """연속 True 블록의 최대 길이 반환."""
    max_block = 0
    current = 0
    for val in synced_series:
        if val:
            current += 1
            max_block = max(max_block, current)
        else:
            current = 0
    return max_block


def _cell_key(lat: float, lon: float) -> tuple[int, int]:
    return (round(lat / CELL_SIZE), round(lon / CELL_SIZE))


def _default_result(mmsi_b: str) -> dict:
    return {
        'pair_detected': False,
        'sync_duration_min': 0.0,
        'max_sync_block_min': 0.0,
        'mean_separation_nm': 0.0,
        'sog_delta_mean': 0.0,
        'cog_delta_mean': 0.0,
        'simultaneous_gap_min': 0.0,
        'g_codes': [],
        'confidence': 0.0,
        'pair_mmsi': mmsi_b,
    }


# ──────────────────────────────────────────────────────────────
# 공개 API
# ──────────────────────────────────────────────────────────────

def detect_pair_trawl(
    df_a: pd.DataFrame,
    df_b: pd.DataFrame,
    mmsi_a: str,
    mmsi_b: str,
    role_a: str = 'UNKNOWN',
    role_b: str = 'UNKNOWN',
    similarity: float = 0.0,
) -> dict:
    """쌍끌이 트롤 공조 탐지 (DAR-03 G-06).

    Args:
        df_a: 선박 A의 AIS DataFrame. 필수 컬럼: timestamp, lat, lon, sog, cog
        df_b: 선박 B의 AIS DataFrame. 필수 컬럼: timestamp, lat, lon, sog, cog
        mmsi_a: 선박 A MMSI
        mmsi_b: 선박 B MMSI (결과의 pair_mmsi에 기록)
        role_a/role_b: 'FISHING'|'CARRIER'|'PT_MAIN'|'PT_SUB'|'UNKNOWN'
        similarity: find_pair_candidates가 계산한 1차 유사도 (0~1)

    Returns:
        위 필드들 + role_a/role_b/similarity/bonus/pair_type
    """
    required_cols = {'timestamp', 'lat', 'lon', 'sog', 'cog'}

    if df_a.empty or df_b.empty:
        logger.debug('pair_trawl(%s, %s): empty DataFrame', mmsi_a, mmsi_b)
        return _default_result(mmsi_b)

    missing_a = required_cols - set(df_a.columns)
    missing_b = required_cols - set(df_b.columns)
    if missing_a or missing_b:
        logger.warning(
            'pair_trawl(%s, %s): missing columns a=%s b=%s',
            mmsi_a, mmsi_b, missing_a, missing_b,
        )
        return _default_result(mmsi_b)

    # ── Step 1: timestamp inner join ────────────────────────
    a = df_a[['timestamp', 'lat', 'lon', 'sog', 'cog']].copy()
    b = df_b[['timestamp', 'lat', 'lon', 'sog', 'cog']].copy()

    a['timestamp'] = pd.to_datetime(a['timestamp'])
    b['timestamp'] = pd.to_datetime(b['timestamp'])

    merged = pd.merge(
        a.rename(columns={'lat': 'lat_a', 'lon': 'lon_a', 'sog': 'sog_a', 'cog': 'cog_a'}),
        b.rename(columns={'lat': 'lat_b', 'lon': 'lon_b', 'sog': 'sog_b', 'cog': 'cog_b'}),
        on='timestamp',
        how='inner',
    ).sort_values('timestamp').reset_index(drop=True)

    total_aligned = len(merged)
    if total_aligned < MIN_SYNC_CYCLES:
        logger.debug(
            'pair_trawl(%s, %s): only %d aligned rows (need %d)',
            mmsi_a, mmsi_b, total_aligned, MIN_SYNC_CYCLES,
        )
        return _default_result(mmsi_b)

    # ── Step 2: 행별 동기화 지표 계산 ───────────────────────
    merged['distance_nm'] = merged.apply(
        lambda r: haversine_nm(r['lat_a'], r['lon_a'], r['lat_b'], r['lon_b']),
        axis=1,
    )
    merged['sog_delta'] = (merged['sog_a'] - merged['sog_b']).abs()
    merged['cog_delta'] = merged.apply(
        lambda r: _cog_delta(r['cog_a'], r['cog_b']),
        axis=1,
    )
    merged['both_in_range'] = (
        merged['sog_a'].between(SOG_MIN, SOG_MAX)
        & merged['sog_b'].between(SOG_MIN, SOG_MAX)
    )
    merged['synced'] = (
        (merged['distance_nm'] <= PROXIMITY_NM)
        & (merged['sog_delta'] <= SOG_DELTA_MAX)
        & (merged['cog_delta'] <= COG_DELTA_MAX)
        & merged['both_in_range']
    )

    # ── Step 3: 연속 블록 탐지 ──────────────────────────────
    max_block_cycles = _max_sync_block(merged['synced'])
    if max_block_cycles < MIN_SYNC_CYCLES:
        logger.debug(
            'pair_trawl(%s, %s): max sync block %d cycles < %d required',
            mmsi_a, mmsi_b, max_block_cycles, MIN_SYNC_CYCLES,
        )
        return _default_result(mmsi_b)

    total_synced = int(merged['synced'].sum())
    sync_duration_min = total_synced * CYCLE_INTERVAL_MIN
    max_sync_block_min = max_block_cycles * CYCLE_INTERVAL_MIN

    mean_separation_nm = float(merged.loc[merged['synced'], 'distance_nm'].mean())
    sog_delta_mean = float(merged.loc[merged['synced'], 'sog_delta'].mean())
    cog_delta_mean = float(merged.loc[merged['synced'], 'cog_delta'].mean())

    # ── Step 4: 동시 AIS 차단 검출 ──────────────────────────
    gaps_a = _find_gaps(df_a, gap_threshold_min=10.0)
    gaps_b = _find_gaps(df_b, gap_threshold_min=10.0)
    simultaneous_gap_min = _overlap_minutes(gaps_a, gaps_b)

    g_codes: list[str] = []
    if simultaneous_gap_min >= SIMULTANEOUS_GAP_MIN:
        g_codes.append('P-01')

    # ── Step 5: 신뢰도 산출 ─────────────────────────────────
    sync_ratio = min(1.0, total_synced / total_aligned)

    synced_distances = merged.loc[merged['synced'], 'distance_nm']
    if len(synced_distances) > 1:
        std_distance = float(synced_distances.std())
    else:
        std_distance = 0.0
    separation_stability = 1.0 - min(1.0, std_distance / PROXIMITY_NM)

    sog_sync_quality = 1.0 - min(1.0, sog_delta_mean / SOG_DELTA_MAX)

    confidence = round(
        sync_ratio * 0.4
        + separation_stability * 0.3
        + sog_sync_quality * 0.3,
        4,
    )

    # ── role 매칭 가점 (DAR-03 선종 정합) ─────────────────────
    bonus = 0
    pair_type = 'GENERIC'
    if role_a == 'PT_MAIN' and role_b == 'PT_SUB':
        bonus = 15; pair_type = 'PT_REGISTERED'
    elif role_b == 'PT_MAIN' and role_a == 'PT_SUB':
        bonus = 15; pair_type = 'PT_REGISTERED'
    elif role_a == 'FISHING' and role_b == 'FISHING':
        bonus = 10; pair_type = 'COOP_FISHING'
    elif {role_a, role_b} == {'FISHING', 'CARRIER'}:
        bonus = 15; pair_type = 'TRANSSHIP_LIKE'

    logger.info(
        'pair_trawl(%s, %s): detected — sync=%.0fmin max_block=%.0fmin '
        'sep=%.3fnm confidence=%.3f bonus=%d type=%s g_codes=%s',
        mmsi_a, mmsi_b,
        sync_duration_min, max_sync_block_min,
        mean_separation_nm, confidence, bonus, pair_type, g_codes,
    )

    return {
        'pair_detected': True,
        'sync_duration_min': round(sync_duration_min, 1),
        'max_sync_block_min': round(max_sync_block_min, 1),
        'mean_separation_nm': round(mean_separation_nm, 4),
        'sog_delta_mean': round(sog_delta_mean, 4),
        'cog_delta_mean': round(cog_delta_mean, 4),
        'simultaneous_gap_min': round(simultaneous_gap_min, 1),
        'g_codes': g_codes,
        'confidence': confidence,
        'pair_mmsi': mmsi_b,
        'role_a': role_a,
        'role_b': role_b,
        'similarity': round(similarity, 4),
        'bonus': bonus,
        'pair_type': pair_type,
    }


def _classify_role(
    mmsi: str,
    info: dict,
    pt_registered: set[str],
    pt_sub_registered: set[str],
) -> str:
    """페어 후보 role 판정. 반환: PT_MAIN/PT_SUB/FISHING/CARRIER/UNKNOWN."""
    if mmsi in pt_sub_registered:
        return 'PT_SUB'
    if mmsi in pt_registered:
        return 'PT_MAIN'
    kind = info.get('ship_kind_code', '') if info else ''
    ship_ty = (info.get('ship_ty') or info.get('vessel_type') or '') if info else ''
    if kind == '000020':
        return 'FISHING'
    if kind in ('000023', '000024'):
        return 'CARRIER'
    # 중국 412* 허가 어선은 ship_kind 없어도 FISHING 간주
    if mmsi.startswith('412'):
        return 'FISHING'
    st = ship_ty.lower() if isinstance(ship_ty, str) else ''
    if any(k in st for k in ('cargo', 'tanker', 'supply', 'carrier')):
        return 'CARRIER'
    if 'fishing' in st:
        return 'FISHING'
    return 'UNKNOWN'


def _trajectory_similarity(
    df_a: pd.DataFrame, df_b: pd.DataFrame, min_samples: int = 6,
) -> tuple[float, int, dict]:
    """두 선박의 공통 타임스탬프 기반 궤적 유사도 (0~1).

    Returns: (similarity, common_samples, breakdown)
    breakdown = {location_score, sog_corr, cog_alignment}
    """
    if df_a.empty or df_b.empty:
        return 0.0, 0, {}
    cols = {'timestamp', 'lat', 'lon', 'sog', 'cog'}
    if cols - set(df_a.columns) or cols - set(df_b.columns):
        return 0.0, 0, {}
    try:
        a = df_a[['timestamp', 'lat', 'lon', 'sog', 'cog']].copy()
        b = df_b[['timestamp', 'lat', 'lon', 'sog', 'cog']].copy()
        a['timestamp'] = pd.to_datetime(a['timestamp'])
        b['timestamp'] = pd.to_datetime(b['timestamp'])
        m = pd.merge(
            a.rename(columns={'lat': 'la', 'lon': 'oa', 'sog': 'sa', 'cog': 'ca'}),
            b.rename(columns={'lat': 'lb', 'lon': 'ob', 'sog': 'sb', 'cog': 'cb'}),
            on='timestamp', how='inner',
        )
    except Exception:
        return 0.0, 0, {}
    n = len(m)
    if n < min_samples:
        return 0.0, n, {}

    # location: 평균 거리 기반 (500m 이내 1.0 → 1km 0.0 선형)
    dists = [
        haversine_nm(r.la, r.oa, r.lb, r.ob)
        for r in m.itertuples(index=False)
    ]
    mean_dist_nm = sum(dists) / len(dists)
    location_score = max(0.0, min(1.0, 1.0 - (mean_dist_nm - PROXIMITY_NM) / PROXIMITY_NM))

    # sog 상관 (Pearson)
    try:
        sa = m['sa'].astype(float)
        sb = m['sb'].astype(float)
        if sa.std() > 0 and sb.std() > 0:
            sog_corr = abs(float(sa.corr(sb)))
        else:
            sog_corr = 1.0 if (sa - sb).abs().mean() < 0.5 else 0.0
    except Exception:
        sog_corr = 0.0

    # cog 방향 일치: 평균 각도차 → 10°=1.0, 90°=0.0 선형
    cog_deltas = [
        _cog_delta(float(r.ca), float(r.cb))
        for r in m.itertuples(index=False)
    ]
    mean_cog_delta = sum(cog_deltas) / len(cog_deltas)
    cog_alignment = max(0.0, min(1.0, 1.0 - (mean_cog_delta - COG_DELTA_MAX) / 80.0))

    similarity = 0.4 * location_score + 0.3 * sog_corr + 0.3 * cog_alignment
    return round(similarity, 4), n, {
        'location_score': round(location_score, 3),
        'sog_corr': round(sog_corr, 3),
        'cog_alignment': round(cog_alignment, 3),
        'mean_distance_nm': round(mean_dist_nm, 4),
        'mean_cog_delta_deg': round(mean_cog_delta, 2),
    }


# bbox 1차 탐색 반경 (도 단위)
BBOX_DEG = 0.01                   # 약 1.1km — 주변 후보만 컷
SIMILARITY_PAIR = 0.70            # 유력 페어
SIMILARITY_OBSERVE = 0.50         # 관찰 페어


def find_pair_candidates(
    base_mmsis: set[str],
    vessel_dfs: dict[str, pd.DataFrame],
    get_vessel_info,
    pt_registered: Optional[set[str]] = None,
    pt_sub_registered: Optional[set[str]] = None,
    bbox_deg: float = BBOX_DEG,
    min_common_samples: int = 6,
    similarity_threshold: float = SIMILARITY_OBSERVE,
) -> list[dict]:
    """base 선박별로 bbox 1차 → 궤적 유사도 2차로 페어 후보 반환.

    base 조건: PT 등록 선박, 어선(000020), 중국 412* 허가선 등 최소 1척 어선 성격.
    target: 인접 격자 내 모든 선박. 궤적 유사도 ≥ similarity_threshold 이면 후보.

    Returns: [{'base_mmsi', 'target_mmsi', 'similarity', 'common_samples',
               'base_role', 'target_role', 'breakdown'}, ...]
    """
    pt_registered = pt_registered or set()
    pt_sub_registered = pt_sub_registered or set()

    # 각 선박의 최근 위치 → grid 빌드
    last_pos: dict[str, dict] = {}
    for mmsi, df in vessel_dfs.items():
        if df.empty or not all(c in df.columns for c in ('lat', 'lon')):
            continue
        try:
            row = df.iloc[-1]
            last_pos[mmsi] = {'lat': float(row['lat']), 'lon': float(row['lon'])}
        except Exception:
            continue

    def _cell(lat: float, lon: float) -> tuple[int, int]:
        return (int(lat / bbox_deg), int(lon / bbox_deg))

    grid: dict[tuple[int, int], list[str]] = {}
    for mmsi, p in last_pos.items():
        grid.setdefault(_cell(p['lat'], p['lon']), []).append(mmsi)

    checked: set[tuple[str, str]] = set()
    results: list[dict] = []

    for base in base_mmsis:
        if base not in last_pos:
            continue
        bp = last_pos[base]
        bc = _cell(bp['lat'], bp['lon'])
        # 인접 9 cell
        neighbors: list[str] = []
        for dr in (-1, 0, 1):
            for dc in (-1, 0, 1):
                neighbors.extend(grid.get((bc[0] + dr, bc[1] + dc), []))
        for target in neighbors:
            if target == base:
                continue
            key = (base, target) if base < target else (target, base)
            if key in checked:
                continue
            checked.add(key)

            sim, n_common, breakdown = _trajectory_similarity(
                vessel_dfs[base], vessel_dfs[target], min_common_samples,
            )
            if sim < similarity_threshold:
                continue

            info_a = get_vessel_info(base) if get_vessel_info else {}
            info_b = get_vessel_info(target) if get_vessel_info else {}
            role_a = _classify_role(base, info_a, pt_registered, pt_sub_registered)
            role_b = _classify_role(target, info_b, pt_registered, pt_sub_registered)

            # 최소 1척이 어선 성격이어야 함
            fishing_like = {'FISHING', 'PT_MAIN', 'PT_SUB'}
            if role_a not in fishing_like and role_b not in fishing_like:
                continue

            results.append({
                'base_mmsi': base,
                'target_mmsi': target,
                'similarity': sim,
                'common_samples': n_common,
                'base_role': role_a,
                'target_role': role_b,
                'breakdown': breakdown,
                'is_strong': sim >= SIMILARITY_PAIR,
            })

    logger.info(
        'find_pair_candidates: base=%d, pool=%d → candidates=%d (strong=%d)',
        len(base_mmsis), len(last_pos), len(results),
        sum(1 for r in results if r['is_strong']),
    )
    return results


def scan_unregistered_pairs(
    vessel_dfs: dict[str, pd.DataFrame],
    registered_pairs: set[tuple[str, str]],
) -> list[tuple[str, str]]:
    """fleet_registry에 없는 TRAWL 선박 중 500m 이내 + 속력 동기화 조건을
    만족하는 쌍 후보 반환. cell-key partitioning으로 O(n²) 회피.

    Args:
        vessel_dfs: mmsi → AIS DataFrame. 각 DataFrame은 timestamp, lat, lon, sog 컬럼 필요
        registered_pairs: 이미 확인된 쌍 (fleet_tracker 제공). 정규화: (smaller, larger)

    Returns:
        list of (mmsi_a, mmsi_b) candidate pairs (정규화된 순서)
    """
    if len(vessel_dfs) < 2:
        return []

    CANDIDATE_PROXIMITY_NM = PROXIMITY_NM * CANDIDATE_PROXIMITY_FACTOR

    # ── 각 선박의 마지막 위치 추출 ──────────────────────────
    last_positions: dict[str, dict] = {}
    for mmsi, df in vessel_dfs.items():
        if df.empty or 'lat' not in df.columns or 'lon' not in df.columns or 'sog' not in df.columns:
            continue
        try:
            last_row = df.sort_values('timestamp').iloc[-1]
            lat = float(last_row['lat'])
            lon = float(last_row['lon'])
            sog = float(last_row['sog'])
        except Exception:
            continue
        last_positions[mmsi] = {'lat': lat, 'lon': lon, 'sog': sog}

    if len(last_positions) < 2:
        return []

    # ── cell-key 격자 구성 ───────────────────────────────────
    cell_map: dict[tuple[int, int], list[str]] = {}
    for mmsi, pos in last_positions.items():
        key = _cell_key(pos['lat'], pos['lon'])
        if key not in cell_map:
            cell_map[key] = []
        cell_map[key].append(mmsi)

    # ── 인접 9셀 내 후보 쌍 탐색 ────────────────────────────
    candidates: list[tuple[str, str]] = []
    checked: set[tuple[str, str]] = set()

    for mmsi_a, pos_a in last_positions.items():
        base_cell = _cell_key(pos_a['lat'], pos_a['lon'])

        # 인접 8셀 + 자기 셀 수집
        neighbor_mmsis: list[str] = []
        for dr in (-1, 0, 1):
            for dc in (-1, 0, 1):
                neighbor_cell = (base_cell[0] + dr, base_cell[1] + dc)
                neighbor_mmsis.extend(cell_map.get(neighbor_cell, []))

        for mmsi_b in neighbor_mmsis:
            if mmsi_b == mmsi_a:
                continue

            # 정규화된 쌍 키
            pair_key: tuple[str, str] = (
                (mmsi_a, mmsi_b) if mmsi_a < mmsi_b else (mmsi_b, mmsi_a)
            )

            # 중복 검사
            if pair_key in checked:
                continue
            checked.add(pair_key)

            # 이미 등록된 쌍 건너뜀
            if pair_key in registered_pairs:
                continue

            pos_b = last_positions.get(mmsi_b)
            if pos_b is None:
                continue

            # 속력 범위 필터 (완화 기준)
            sog_a = pos_a['sog']
            sog_b = pos_b['sog']
            if not (CANDIDATE_SOG_MIN <= sog_a <= CANDIDATE_SOG_MAX):
                continue
            if not (CANDIDATE_SOG_MIN <= sog_b <= CANDIDATE_SOG_MAX):
                continue

            # 거리 필터
            dist_nm = haversine_nm(pos_a['lat'], pos_a['lon'], pos_b['lat'], pos_b['lon'])
            if dist_nm > CANDIDATE_PROXIMITY_NM:
                continue

            candidates.append(pair_key)

    logger.debug(
        'scan_unregistered_pairs: %d vessels, %d candidates found',
        len(last_positions), len(candidates),
    )
    return candidates
