"""Behavioral Anomaly Scorer (DNS) — SQLite-backed.

Adapted from PostgreSQL to use SQLite via soagila.db.sqlite_init.
All BAS calculation logic preserved exactly as-is.
"""

import json
import logging
import math
from datetime import datetime, timezone
from uuid import uuid4
from collections import Counter

from soagila.db.sqlite_init import get_db
from soagila.db.event_bus import publish_event
from soagila.core.dns.models import AccessEvent, BehavioralProfileModel

logger = logging.getLogger("soagila.dns.scorer")

CHANNEL = "soagila:metrics"

# In-memory profile cache
_profiles: dict[str, BehavioralProfileModel] = {}
# Event history per user (last 100 events)
_event_history: dict[str, list[AccessEvent]] = {}

# BAS graduated response thresholds
BAS_ACTIONS = [
    (0, 30, "logged"),
    (30, 50, "enhanced_monitoring"),
    (50, 70, "session_restricted"),
    (70, 85, "mfa_required"),
    (85, 100, "frozen"),
]


def get_action_for_score(score: float) -> str:
    """Get the graduated response action for a BAS score."""
    for low, high, action in BAS_ACTIONS:
        if low <= score < high:
            return action
    return "frozen"


async def get_profile(user_id: str) -> BehavioralProfileModel:
    """Get or create a behavioral profile for a user."""
    if user_id in _profiles:
        return _profiles[user_id]

    # Try loading from SQLite
    db = await get_db()
    cursor = await db.execute(
        """
        SELECT avg_session_duration, typical_work_hours,
               avg_queries_per_session, common_topics,
               baseline_updated_at
        FROM behavioral_profiles WHERE user_id = ?
        """,
        (user_id,),
    )
    row = await cursor.fetchone()

    if row:
        work_hours = row[1]
        if isinstance(work_hours, str):
            work_hours = json.loads(work_hours)
        common_topics = row[3]
        if isinstance(common_topics, str):
            common_topics = json.loads(common_topics)

        profile = BehavioralProfileModel(
            user_id=user_id,
            avg_session_duration_minutes=row[0] or 30.0,
            typical_work_hours=work_hours if work_hours else {"start": "09:00", "end": "18:00"},
            avg_queries_per_session=row[2] or 10.0,
            common_topics=common_topics if common_topics else [],
            baseline_updated_at=row[4] or datetime.now(timezone.utc),
        )
    else:
        profile = BehavioralProfileModel(user_id=user_id)

    _profiles[user_id] = profile
    return profile


async def update_profile(user_id: str, event: AccessEvent):
    """Update behavioral baseline with a new event."""
    profile = await get_profile(user_id)

    # Track event history
    if user_id not in _event_history:
        _event_history[user_id] = []
    _event_history[user_id].append(event)
    # Keep last 200 events
    if len(_event_history[user_id]) > 200:
        _event_history[user_id] = _event_history[user_id][-200:]

    # Update topic statistics
    if event.topic and event.topic not in profile.common_topics:
        profile.common_topics.append(event.topic)
        profile.common_topics = profile.common_topics[-20:]  # Keep top 20

    # Update event frequency
    profile.total_events += 1

    # Recalculate avg events per hour from history
    history = _event_history[user_id]
    if len(history) >= 5:
        time_span = (history[-1].timestamp - history[0].timestamp).total_seconds() / 3600
        if time_span > 0:
            profile.avg_events_per_hour = len(history) / time_span

    profile.baseline_updated_at = datetime.now(timezone.utc)
    _profiles[user_id] = profile

    # Persist periodically (every 10 events)
    if profile.total_events % 10 == 0:
        await _persist_profile(profile)


async def _persist_profile(profile: BehavioralProfileModel):
    """Save behavioral profile to SQLite using upsert."""
    db = await get_db()
    now = datetime.now(timezone.utc).isoformat()

    await db.execute(
        """
        INSERT INTO behavioral_profiles (
            id, user_id, avg_session_duration,
            typical_work_hours, avg_queries_per_session, common_topics,
            baseline_updated_at, created_at
        ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
        ON CONFLICT (user_id) DO UPDATE SET
            avg_session_duration = ?,
            typical_work_hours = ?,
            avg_queries_per_session = ?,
            common_topics = ?,
            baseline_updated_at = ?
        """,
        (
            str(uuid4()),
            profile.user_id,
            profile.avg_session_duration_minutes,
            json.dumps(profile.typical_work_hours),
            profile.avg_queries_per_session,
            json.dumps(profile.common_topics),
            profile.baseline_updated_at.isoformat() if isinstance(profile.baseline_updated_at, datetime) else str(profile.baseline_updated_at),
            now,
            # ON CONFLICT params
            profile.avg_session_duration_minutes,
            json.dumps(profile.typical_work_hours),
            profile.avg_queries_per_session,
            json.dumps(profile.common_topics),
            profile.baseline_updated_at.isoformat() if isinstance(profile.baseline_updated_at, datetime) else str(profile.baseline_updated_at),
        ),
    )
    await db.commit()


async def calculate_bas(user_id: str, current_event: AccessEvent) -> float:
    """
    Calculate Behavioral Anomaly Score (BAS) -- 0 to 100.
    Four factors, each 0-25 points:
    1. Time of day deviation
    2. Request frequency deviation
    3. Topic deviation
    4. Session pattern deviation
    """
    profile = await get_profile(user_id)
    history = _event_history.get(user_id, [])

    score = 0.0

    # Factor 1: Time of Day Deviation (0-25)
    score += _time_deviation_score(current_event, profile)

    # Factor 2: Request Frequency Deviation (0-25)
    score += _frequency_deviation_score(current_event, profile, history)

    # Factor 3: Topic Deviation (0-25)
    score += _topic_deviation_score(current_event, profile)

    # Factor 4: Session Pattern Deviation (0-25)
    score += _session_pattern_score(current_event, profile, history)

    # Clamp to [0, 100]
    score = max(0.0, min(100.0, score))

    return round(score, 2)


def _time_deviation_score(event: AccessEvent, profile: BehavioralProfileModel) -> float:
    """Score based on how unusual the time of access is."""
    hour = event.timestamp.hour

    # Parse work hours
    try:
        start_hour = int(profile.typical_work_hours.get("start", "09:00").split(":")[0])
        end_hour = int(profile.typical_work_hours.get("end", "18:00").split(":")[0])
    except (ValueError, AttributeError):
        start_hour, end_hour = 9, 18

    # Within work hours = no deviation
    if start_hour <= hour <= end_hour:
        return 0.0

    # Calculate distance from nearest work hour boundary
    dist_start = min(abs(hour - start_hour), 24 - abs(hour - start_hour))
    dist_end = min(abs(hour - end_hour), 24 - abs(hour - end_hour))
    min_dist = min(dist_start, dist_end)

    # Scale: 1 hour outside = 5 points, max 25
    return min(min_dist * 5.0, 25.0)


def _frequency_deviation_score(
    event: AccessEvent,
    profile: BehavioralProfileModel,
    history: list[AccessEvent],
) -> float:
    """Score based on request frequency vs baseline."""
    if len(history) < 3:
        return 0.0  # Not enough data

    # Count events in last 10 minutes
    cutoff = event.timestamp.timestamp() - 600
    recent = [e for e in history if e.timestamp.timestamp() > cutoff]
    recent_rate = len(recent) * 6  # Extrapolate to per-hour

    if profile.avg_events_per_hour <= 0:
        return 0.0

    ratio = recent_rate / profile.avg_events_per_hour

    # Normal: ratio 0.5-2.0, anomalous above 3.0
    if ratio <= 2.0:
        return 0.0
    elif ratio <= 5.0:
        return (ratio - 2.0) / 3.0 * 15.0
    else:
        return min(15.0 + (ratio - 5.0) * 2.0, 25.0)


def _topic_deviation_score(event: AccessEvent, profile: BehavioralProfileModel) -> float:
    """Score based on whether the topic is unusual for this user."""
    if not event.topic or not profile.common_topics:
        return 0.0

    if event.topic in profile.common_topics:
        return 0.0

    # Unknown topic = mild anomaly
    return 10.0


def _session_pattern_score(
    event: AccessEvent,
    profile: BehavioralProfileModel,
    history: list[AccessEvent],
) -> float:
    """Score based on session duration and pattern anomalies."""
    if len(history) < 2:
        return 0.0

    # Check for rapid-fire requests (< 1 second between events)
    if len(history) >= 2:
        last_gap = (event.timestamp - history[-1].timestamp).total_seconds()
        if last_gap < 0.5:
            return 15.0  # Likely automated
        elif last_gap < 1.0:
            return 8.0

    return 0.0
