"""Read-only analysis handlers — patterns, temporal, FFT, TLS health, geo, entity detail."""
import traceback
from analysis.constants import LOW_MEMORY_THRESHOLD_ROWS
import numpy as np
import polars as pl

from ..session_store import SessionStore
from ._helpers import _resolve_dataset, _count_rows, NUMERIC_TYPE_NAMES
import logging
logger = logging.getLogger(__name__)


async def _handle_analyze_patterns(dataset_id: str, top_n: int = 10) -> dict:
    """Read-only: traffic pattern analysis."""
    entry, err = _resolve_dataset(dataset_id)
    if err:
        return err
    lf = entry['lazyframe']
    schema = entry.get('schema', {})
    n = _count_rows(lf)
    df = lf.head(LOW_MEMORY_THRESHOLD_ROWS).collect(streaming=True)

    result = {'total_rows': n, 'sampled': min(n, LOW_MEMORY_THRESHOLD_ROWS)}

    for col_name, label in [(':ips', 'src_ips'), (':ipd', 'dst_ips'),
                              ('snam', 'snis'), ('cnam', 'cert_names')]:
        if col_name in df.columns:
            try:
                top = df[col_name].value_counts(sort=True).head(top_n)
                result[label] = {
                    'top': [{'value': str(r[col_name]), 'count': int(r['count'])}
                            for r in top.iter_rows(named=True)],
                    'unique': int(df[col_name].n_unique()),
                }
            except Exception:
                logger.error("_handle_analyze_patterns failed: {}".format(traceback.format_exc()))
                pass

    for port_col in [':prs', ':prd']:
        if port_col in df.columns:
            try:
                top = df[port_col].value_counts(sort=True).head(top_n)
                result[f'{port_col}_dist'] = {
                    'top': [{'port': int(r[port_col]), 'count': int(r['count'])}
                            for r in top.iter_rows(named=True)],
                    'unique': int(df[port_col].n_unique()),
                }
            except Exception:
                logger.error("unknown failed: {}".format(traceback.format_exc()))
                pass

    for tls_col in ['0ver']:
        if tls_col in df.columns:
            try:
                vc = df[tls_col].value_counts(sort=True)
                result['tls_versions'] = [{'version': str(r[tls_col]), 'count': int(r['count'])}
                                          for r in vc.iter_rows(named=True)]
            except Exception:
                logger.error("unknown failed: {}".format(traceback.format_exc()))
                pass

    return result


async def _handle_analyze_temporal(dataset_id: str, timestamp_column: str = None) -> dict:
    """Read-only: temporal traffic pattern analysis."""
    entry, err = _resolve_dataset(dataset_id)
    if err:
        return err
    lf = entry['lazyframe']
    schema = entry.get('schema', {})

    if not timestamp_column:
        for cand in ['timestamp', 'time', '8dbd', '8did']:
            if cand in schema:
                timestamp_column = cand
                break
    if not timestamp_column:
        return {'error': 'No timestamp column found', 'truncated': False}

    n = _count_rows(lf)
    return {'total_rows': n, 'timestamp_column': timestamp_column,
            'note': 'Temporal analysis requires parsed timestamps; raw values shown in explore_distributions'}


async def _handle_analyze_fft(
    dataset_id: str,
    timestamp_column: str,
    top_n: int = 5,
) -> dict:
    """FFT spectrum analysis on a timestamp column to detect periodic patterns."""
    entry, err = _resolve_dataset(dataset_id)
    if err:
        return err

    lf: pl.LazyFrame = entry['lazyframe']
    schema = entry.get('schema', {})

    col = None
    if timestamp_column in schema:
        col = timestamp_column
    else:
        col_lower = timestamp_column.lower()
        for c in schema:
            if c.lower() == col_lower:
                col = c
                break
    if col is None:
        return {'error': f"Timestamp column '{timestamp_column}' not found in dataset. Available: {list(schema.keys())[:20]}",
                'truncated': False}

    df = lf.select(pl.col(col).alias('__ts__')).collect(streaming=True)
    n_raw = len(df)
    if n_raw < 4:
        return {'error': f'Need at least 4 samples for FFT; got {n_raw}', 'truncated': False}

    ts_series = df['__ts__']
    dtype = ts_series.dtype
    str_dtypes = (pl.Utf8, pl.String)

    if dtype in str_dtypes:
        try:
            ts_parsed = ts_series.str.strptime(pl.Datetime, format=None, strict=False)
            ts_numeric = ts_parsed.cast(pl.Int64) / 1_000_000_000.0
        except Exception:
            logger.error("unknown failed: {}".format(traceback.format_exc()))
            try:
                ts_numeric = ts_series.cast(pl.Float64)
            except Exception as e:
                return {'error': f'Cannot convert string column to numeric or datetime: {e}',
                        'truncated': False}
    elif dtype in (pl.Datetime, pl.Date):
        ts_numeric = ts_series.cast(pl.Int64) / 1_000_000_000.0
    else:
        try:
            ts_numeric = ts_series.cast(pl.Float64)
        except Exception as e:
            return {'error': f'Cannot cast column to float: {e}', 'truncated': False}

    df_sorted = pl.DataFrame({'__val__': ts_numeric}).drop_nulls().sort('__val__')
    n = len(df_sorted)
    if n < 4:
        return {'error': f'After removing nulls, only {n} samples remain (need ≥4)', 'truncated': False}

    values = df_sorted['__val__'].to_numpy().astype(np.float64)

    values_centered = values - np.mean(values)
    fft_complex = np.fft.fft(values_centered)
    fft_magnitudes = np.abs(fft_complex)
    freqs = np.fft.fftfreq(n)

    pos_mask = freqs > 0
    freqs_pos = freqs[pos_mask]
    mags_pos = fft_magnitudes[pos_mask]

    if len(freqs_pos) == 0:
        return {'error': 'Insufficient positive frequencies for FFT analysis', 'truncated': False}

    peak_indices = np.argsort(mags_pos)[::-1][:min(top_n, len(mags_pos))]
    top_periods = []
    for idx in peak_indices:
        freq = float(freqs_pos[idx])
        mag = float(mags_pos[idx])
        period = float(1.0 / freq) if freq > 1e-10 else float('inf')
        if n > 1:
            time_span = float(values[-1] - values[0])
            sample_period = time_span / max(n - 1, 1)
            time_period = period * sample_period
        else:
            time_period = float('inf')
        top_periods.append({
            'period': round(period, 2),
            'magnitude': round(mag, 4),
            'frequency': round(freq, 6),
            'time_domain_period_seconds': round(time_period, 2) if time_period != float('inf') else None,
        })

    mag_sum = float(np.sum(mags_pos))
    if mag_sum > 0:
        spectral_centroid = float(np.sum(freqs_pos * mags_pos) / mag_sum)
    else:
        spectral_centroid = 0.0

    mean_mag = float(np.mean(mags_pos)) if len(mags_pos) > 0 else 1.0
    max_mag = float(mags_pos[peak_indices[0]]) if len(peak_indices) > 0 else 0.0
    periodicity_score = round(max_mag / max(mean_mag, 1e-10), 4)

    decimate_step = max(1, len(freqs_pos) // 200)
    freqs_decimated = freqs_pos[::decimate_step].tolist()
    mags_decimated = mags_pos[::decimate_step].tolist()

    return {
        'frequencies': [round(float(f), 6) for f in freqs_decimated],
        'magnitudes': [round(float(m), 4) for m in mags_decimated],
        'top_periods': top_periods,
        'spectral_centroid': round(spectral_centroid, 6),
        'periodicity_score': periodicity_score,
        'n_samples': n,
        'n_raw': n_raw,
        'nan_dropped': n_raw - n,
        'timestamp_column': col,
    }


async def _handle_analyze_tls_health(dataset_id: str) -> dict:
    """Read-only: TLS security assessment."""
    entry, err = _resolve_dataset(dataset_id)
    if err:
        return err
    lf = entry['lazyframe']
    schema = entry.get('schema', {})
    n = _count_rows(lf)
    df = lf.head(LOW_MEMORY_THRESHOLD_ROWS).collect(streaming=True)

    result = {'total_rows': n, 'sampled': min(n, LOW_MEMORY_THRESHOLD_ROWS)}

    if '0ver' in df.columns:
        vc = df['0ver'].value_counts(sort=True)
        versions = {}
        for r in vc.iter_rows(named=True):
            v = str(r['0ver']).replace(' ', '')
            if v in ('0303', '0304'):
                versions['tls_1_2_plus'] = versions.get('tls_1_2_plus', 0) + int(r['count'])
            else:
                versions['tls_legacy'] = versions.get('tls_legacy', 0) + int(r['count'])
        total = sum(versions.values())
        result.update({
            'tls_1_2_plus_pct': round(versions.get('tls_1_2_plus', 0) / max(total, 1) * 100, 1),
            'tls_legacy_pct': round(versions.get('tls_legacy', 0) / max(total, 1) * 100, 1),
            'tls_legacy_versions': [{'version': str(r['0ver']), 'count': int(r['count'])}
                                     for r in vc.iter_rows(named=True)
                                     if str(r['0ver']).replace(' ', '') not in ('0303', '0304')],
        })

    if 'snam' in df.columns:
        empty_sni = df['snam'].is_null().sum() + df['snam'].cast(pl.Utf8).is_in(['', 'null', 'None']).sum()
        result['sni_missing_pct'] = round(float(empty_sni) / max(len(df), 1) * 100, 1)

    if 'cipher-suite' in df.columns:
        weak_ciphers = ['rc4', 'cbc', 'null', 'export']
        total_ciphers = len(df)
        weak_count = 0
        for w in weak_ciphers:
            try:
                weak_count += df['cipher-suite'].str.to_lowercase().str.contains(w).sum()
            except Exception:
                logger.error("unknown failed: {}".format(traceback.format_exc()))
                pass
        result['weak_cipher_pct'] = round(float(weak_count) / max(total_ciphers, 1) * 100, 1) if total_ciphers else 0

    if result.get('tls_legacy_pct', 0) > 20:
        result['warning'] = 'High proportion of legacy TLS — possible downgrade attacks'
    if result.get('sni_missing_pct', 0) > 50:
        result['warning'] = 'Most flows lack SNI — possible IP-direct connections (proxy/VPN)'
    return result


async def _handle_analyze_geo_distribution(dataset_id: str) -> dict:
    """Read-only: geographic traffic analysis."""
    entry, err = _resolve_dataset(dataset_id)
    if err:
        return err
    lf = entry['lazyframe']
    schema = entry.get('schema', {})
    n = _count_rows(lf)
    df = lf.head(LOW_MEMORY_THRESHOLD_ROWS).collect(streaming=True)

    result = {'total_rows': n, 'sampled': min(n, LOW_MEMORY_THRESHOLD_ROWS)}

    for geo_col, label in [('scnt', 'src_countries'), ('dcnt', 'dst_countries')]:
        if geo_col in df.columns:
            try:
                vc = df[geo_col].value_counts(sort=True).head(15)
                result[label] = [{'country': str(r[geo_col]), 'count': int(r['count'])}
                                 for r in vc.iter_rows(named=True)]
                result[f'{label}_unique'] = int(df[geo_col].n_unique())
            except Exception:
                logger.error("_handle_analyze_geo_distribution failed: {}".format(traceback.format_exc()))
                pass

    if 'scnt' in df.columns and 'dcnt' in df.columns:
        try:
            same = (df['scnt'] == df['dcnt']).sum()
            result['domestic_pct'] = round(float(same) / max(len(df), 1) * 100, 1)
            result['international_pct'] = round(100.0 - result['domestic_pct'], 1)
        except Exception:
            logger.error("_handle_analyze_geo_distribution failed: {}".format(traceback.format_exc()))
            pass

    return result


async def _handle_analyze_entity_detail(dataset_id: str, entity_value: str) -> dict:
    """Read-only: deep-dive into a single entity."""
    entry, err = _resolve_dataset(dataset_id)
    if err:
        return err
    lf = entry['lazyframe']
    schema = entry.get('schema', {})

    entity_col = None
    for c in schema:
        if c.startswith('_') or schema[c].split('(')[0].strip() in NUMERIC_TYPE_NAMES or '.' in c:
            continue
        entity_col = c
        break
    if not entity_col:
        entity_col = list(schema.keys())[0]

    filtered = lf.filter(pl.col(entity_col).cast(pl.Utf8).str.contains(entity_value, literal=True))
    n = _count_rows(filtered)
    if n == 0:
        return {'error': f"Entity '{entity_value}' not found in column '{entity_col}'", 'truncated': False}
    df = filtered.collect(streaming=True)

    result = {
        'entity_value': entity_value,
        'entity_column': entity_col,
        'flow_count': n,
        'columns': list(df.columns[:15]),
    }

    for c in df.columns:
        if df[c].dtype in (pl.Float32, pl.Float64, pl.Int32, pl.Int64):
            try:
                result[f'{c}_stats'] = {
                    'mean': round(float(df[c].mean()), 2),
                    'min': float(df[c].min()) if df[c].min() is not None else None,
                    'max': float(df[c].max()) if df[c].max() is not None else None,
                }
            except Exception:
                logger.error("unknown failed: {}".format(traceback.format_exc()))
                pass

    return result
