#!/usr/bin/env python3
"""
Shared monitoring core used by whp-log-monitor-ai.py and whp-monitor-poll.py.

Provides:
  - compute_signature(): deterministic SHA-256 signature for dedup
  - ten_minute_bucket(): 600s bucket of a unix timestamp
  - connect_mysql(): PDO-equivalent pymysql connection using /root/.my.cnf
  - connect_sqlite(): opens /var/lib/whp-ai-monitor/monitor.db
  - load_active_subscriptions(): returns list of subscribed primary domains
  - is_suppressed(): check if a signature is currently suppressed
  - insert_or_increment_issue(): idempotent issue insertion with dedup
  - call_ai(): basic/advanced model call via LiteLLM endpoint
"""
import hashlib
import json
import os
import sqlite3
import time
import configparser

try:
    import pymysql
except ImportError:
    pymysql = None  # type: ignore

try:
    import requests
except ImportError:
    requests = None  # type: ignore

AI_CONFIG_PATH = '/etc/whp/ai-monitor.conf'
AI_KEY_PATH = '/etc/whp/ai-monitor.key'
SQLITE_PATH = '/var/lib/whp-ai-monitor/monitor.db'
SETTINGS_JSON_PATH = '/docker/whp/settings.json'

# ---------- Signatures ----------

def ten_minute_bucket(ts: int) -> int:
    """Floor a unix timestamp to the nearest 600s."""
    return (ts // 600) * 600

def compute_signature(kind: str, **kwargs) -> str:
    """
    Deterministic signature for dedup.
      kind='health' + subsystem + severity  -> server-wide
      kind='site'   + site_id + category + bucket -> site-linked
    """
    if kind == 'health':
        raw = f"health:{kwargs['subsystem']}:{kwargs['severity']}"
    elif kind == 'site':
        raw = f"site:{kwargs['site_id']}:{kwargs['category']}:{kwargs['bucket']}"
    else:
        raise ValueError(f"unknown signature kind: {kind}")
    return hashlib.sha256(raw.encode('utf-8')).hexdigest()

# ---------- DB connections ----------

def _read_mysql_creds():
    """Parse /root/.my.cnf [client] section."""
    cp = configparser.ConfigParser()
    cp.read('/root/.my.cnf')
    s = cp['client']
    return {
        'host': s.get('host', '127.0.0.1'),
        'port': int(s.get('port', '3306')),
        'user': s.get('user'),
        'password': (s.get('password') or '').strip().strip('"').strip("'"),
    }

def connect_mysql():
    if pymysql is None:
        raise RuntimeError(
            "pymysql not installed; install with: pip install pymysql")
    creds = _read_mysql_creds()
    return pymysql.connect(
        host=creds['host'], port=creds['port'],
        user=creds['user'], password=creds['password'],
        database='whp', charset='utf8mb4', autocommit=False,
        cursorclass=pymysql.cursors.DictCursor,
    )

def connect_sqlite():
    conn = sqlite3.connect(SQLITE_PATH)
    conn.row_factory = sqlite3.Row
    return conn

# ---------- Settings ----------

def customer_monitoring_enabled() -> bool:
    """Read global toggle from /docker/whp/settings.json."""
    try:
        with open(SETTINGS_JSON_PATH) as f:
            data = json.load(f)
        cm = data.get('customer_monitoring') or {}
        return bool(cm.get('enabled'))
    except (FileNotFoundError, json.JSONDecodeError):
        return False

def load_active_subscriptions(mysql_conn) -> list:
    """Return list of dicts with keys {primary_domain, site_id, username}
    for all ai_monitor_subscriptions in status='active'."""
    with mysql_conn.cursor() as cur:
        cur.execute("""
            SELECT DISTINCT d.domain_name AS primary_domain, s.id AS site_id, s.username
            FROM whp.ai_monitor_subscriptions sub
            JOIN whp.sites s ON sub.site_id = s.id
            JOIN whp.domains d ON s.primary_domain_id = d.id
            WHERE sub.status = 'active'
        """)
        return cur.fetchall()

# ---------- Suppressions ----------

def is_suppressed(mysql_conn, site_id, signature) -> bool:
    """Check active suppression (site_id may be None for server-wide)."""
    with mysql_conn.cursor() as cur:
        if site_id is None:
            cur.execute(
                "SELECT id FROM whp.ai_monitor_suppressions "
                "WHERE signature=%s AND site_id IS NULL LIMIT 1",
                (signature,))
        else:
            cur.execute(
                "SELECT id FROM whp.ai_monitor_suppressions "
                "WHERE signature=%s AND site_id=%s LIMIT 1",
                (signature, site_id))
        return cur.fetchone() is not None

def bump_suppression_counter(mysql_conn, site_id, signature):
    with mysql_conn.cursor() as cur:
        if site_id is None:
            cur.execute(
                "UPDATE whp.ai_monitor_suppressions "
                "SET suppressed_occurrence_count = suppressed_occurrence_count + 1, "
                "    last_suppressed_event_at = NOW() "
                "WHERE signature=%s AND site_id IS NULL",
                (signature,))
        else:
            cur.execute(
                "UPDATE whp.ai_monitor_suppressions "
                "SET suppressed_occurrence_count = suppressed_occurrence_count + 1, "
                "    last_suppressed_event_at = NOW() "
                "WHERE signature=%s AND site_id=%s",
                (signature, site_id))
    mysql_conn.commit()

# ---------- Issue insertion ----------

def insert_or_increment_issue(mysql_conn, *, signature, site_id, subscription_id,
                              username, container_name,
                              severity, category, subsystem, source,
                              title, diagnosis, recommendation,
                              raw_log_excerpt, context_data):
    """
    Idempotent Issue write. Owns its own transaction — commits on success,
    rolls back on any exception.

      - If suppressed: bump counter, return (None, 'suppressed').
      - If open issue exists with same signature: increment occurrence_count,
        append source to sources JSON, return (id, 'incremented').
      - Otherwise: insert new row, return (id, 'created').

    Note: concurrent pollers can race on the SELECT → INSERT path. Follow-up
    will add a generated-column unique index or advisory lock. For now, an
    accidental duplicate is acceptable (two open rows; can be merged manually).
    """
    if is_suppressed(mysql_conn, site_id, signature):
        bump_suppression_counter(mysql_conn, site_id, signature)
        return (None, 'suppressed')

    try:
        with mysql_conn.cursor() as cur:
            cur.execute(
                "SELECT id, sources FROM whp.ai_monitor_issues "
                "WHERE signature=%s AND status='open' LIMIT 1",
                (signature,))
            row = cur.fetchone()

            if row:
                existing_sources = json.loads(row['sources'] or '[]')
                if source not in existing_sources:
                    existing_sources.append(source)
                cur.execute(
                    "UPDATE whp.ai_monitor_issues "
                    "SET occurrence_count = occurrence_count + 1, "
                    "    last_seen = NOW(), "
                    "    sources = %s "
                    "WHERE id=%s",
                    (json.dumps(existing_sources), row['id']))
                mysql_conn.commit()
                return (row['id'], 'incremented')

            cur.execute(
                """INSERT INTO whp.ai_monitor_issues
                    (subscription_id, site_id, username, container_name,
                     severity, category, subsystem,
                     source, sources, title, diagnosis, recommendation,
                     raw_log_excerpt, context_data, signature,
                     occurrence_count, first_seen, last_seen,
                     status, email_sent)
                   VALUES (%s, %s, %s, %s,
                           %s, %s, %s,
                           %s, %s, %s, %s, %s,
                           %s, %s, %s,
                           1, NOW(), NOW(),
                           'open', 0)""",
                (subscription_id, site_id, username, container_name,
                 severity, category, subsystem,
                 source, json.dumps([source]), title, diagnosis, recommendation,
                 raw_log_excerpt, json.dumps(context_data) if context_data else None,
                 signature))
            new_id = cur.lastrowid
        mysql_conn.commit()
        return (new_id, 'created')
    except Exception:
        try:
            mysql_conn.rollback()
        except Exception:
            pass
        raise

def mark_auto_resolved(mysql_conn, signature):
    """
    System-observed recovery: close the matching open issue.
    Sets status='fixed', auto_resolved_at=NOW(), status_changed_by='system',
    status_changed_at=NOW() on the open row with this signature.
    """
    with mysql_conn.cursor() as cur:
        cur.execute(
            "UPDATE whp.ai_monitor_issues "
            "SET status = 'fixed', "
            "    auto_resolved_at = COALESCE(auto_resolved_at, NOW()), "
            "    status_changed_by = 'system', "
            "    status_changed_at = NOW() "
            "WHERE signature=%s AND status='open'",
            (signature,))
    mysql_conn.commit()

# ---------- AI ----------

def _read_ai_config():
    cp = configparser.ConfigParser()
    cp.read(AI_CONFIG_PATH)
    return {
        'base_url': cp.get('ai', 'base_url', fallback='http://localhost:4000/v1'),
        'basic_model': cp.get('ai', 'basic_model',
                              fallback='global.anthropic.claude-haiku-4-5-20251001-v1:0'),
        'advanced_model': cp.get('ai', 'advanced_model',
                                 fallback='global.anthropic.claude-sonnet-4-6'),
    }

def _read_ai_key():
    with open(AI_KEY_PATH) as f:
        return f.read().strip()

def call_ai(prompt: str, tier: str = 'basic', max_tokens: int = 800, timeout: int = 30) -> dict:
    """
    tier: 'basic' or 'advanced'. Returns {'content': str, 'model': str, 'tokens': int}.
    Raises requests.exceptions on HTTP errors.
    """
    if requests is None:
        raise RuntimeError(
            "requests not installed; install with: pip install requests")
    cfg = _read_ai_config()
    model = cfg['basic_model'] if tier == 'basic' else cfg['advanced_model']
    resp = requests.post(
        f"{cfg['base_url'].rstrip('/')}/chat/completions",
        headers={'Authorization': f'Bearer {_read_ai_key()}',
                 'Content-Type': 'application/json'},
        json={
            'model': model,
            'messages': [{'role': 'user', 'content': prompt}],
            'max_tokens': max_tokens,
        },
        timeout=timeout,
    )
    resp.raise_for_status()
    j = resp.json()
    return {
        'content': j['choices'][0]['message']['content'],
        'model': model,
        'tokens': j.get('usage', {}).get('total_tokens', 0),
    }
