"""Agent tool registry + implementations for the JAE-AI agentic chat.

Register once on import. Each Tool carries:
  - JSON schema for Venice function-calling
  - Handler callable
  - Minimum tier
  - Rate limit spec ('N/min' or 'N/hour')
  - Timeout (seconds)
"""
import json
import time
import re
from collections import defaultdict, deque
from dataclasses import dataclass, field
from pathlib import Path
from typing import Callable, Any

import requests as req

DATA_DIR = Path(__file__).parent / 'data'


@dataclass
class Tool:
    name: str
    description: str
    params: dict
    handler: Callable[[dict], Any]
    tier: str = 'anonymous'
    rate_limit: str = '60/min'
    timeout: int = 30


TOOLS: dict[str, Tool] = {}


def register(tool: Tool):
    TOOLS[tool.name] = tool


# ── Rate limiting (per (identity, tool) key) ─────────────────────────────
_rate_state: dict[tuple, deque] = defaultdict(lambda: deque(maxlen=200))


def parse_spec(spec: str) -> tuple[int, int]:
    """'10/min' -> (10, 60). Supports /sec /min /hour /day."""
    try:
        n, unit = spec.split('/')
        n = int(n)
    except Exception:
        return 60, 60
    unit = unit.lower().strip()
    period = {'sec': 1, 'second': 1, 'min': 60, 'minute': 60,
              'hour': 3600, 'hr': 3600, 'day': 86400}.get(unit, 60)
    return n, period


def check_rate(key: tuple, spec: str) -> bool:
    limit, period = parse_spec(spec)
    now = time.time()
    q = _rate_state[key]
    while q and q[0] < now - period:
        q.popleft()
    if len(q) >= limit:
        return False
    q.append(now)
    return True


# ── Simple TTL cache ─────────────────────────────────────────────────────
_cache: dict[str, tuple[float, Any]] = {}


def cache_get(key: str, ttl: float) -> Any | None:
    entry = _cache.get(key)
    if entry and time.time() - entry[0] < ttl:
        return entry[1]
    return None


def cache_set(key: str, value: Any):
    _cache[key] = (time.time(), value)


def _load_json(name: str) -> Any:
    try:
        with open(DATA_DIR / name) as f:
            return json.load(f)
    except Exception:
        return None


# ── Tool Handlers ────────────────────────────────────────────────────────

def tool_search_site(args: dict) -> dict:
    query = (args.get('query') or '').strip().lower()
    if not query:
        return {'error': 'query required', 'results': []}
    results = []
    # Index main content files
    sources = [
        ('changelog', 'changelog.json'),
        ('navigation', 'navigation.json'),
        ('homepage', 'homepage.json'),
        ('managed_services', 'managed_services.json'),
        ('posts', 'posts.json'),
        ('links', 'links.json'),
        ('tracks', 'tracks.json'),
    ]
    for label, fname in sources:
        data = _load_json(fname)
        if data is None:
            continue
        blob = json.dumps(data, ensure_ascii=False).lower()
        if query in blob:
            # Find first match snippet
            idx = blob.find(query)
            snippet = blob[max(0, idx - 80): idx + 160]
            results.append({
                'source': label,
                'title': label.replace('_', ' ').title(),
                'snippet': snippet.replace('\n', ' '),
                'url': f'/api/{label}' if label != 'homepage' else '/',
            })
    return {'results': results[:5], 'count': len(results)}


def tool_get_sol_price(args: dict) -> dict:
    cached = cache_get('sol_price', 30)
    if cached is not None:
        return cached
    try:
        r = req.get('https://api.binance.com/api/v3/ticker/24hr',
                    params={'symbol': 'SOLUSDT'}, timeout=8)
        r.raise_for_status()
        j = r.json()
        out = {
            'symbol': 'SOL',
            'price_usd': float(j.get('lastPrice', 0)),
            'change_24h_pct': float(j.get('priceChangePercent', 0)),
            'volume_24h': float(j.get('volume', 0)),
            'source': 'binance',
        }
        cache_set('sol_price', out)
        return out
    except Exception as e:
        return {'error': f'price fetch failed: {e}'}


def tool_get_crypto_price(args: dict) -> dict:
    symbol = (args.get('symbol') or '').upper().strip()
    if not symbol or not re.match(r'^[A-Z0-9]{2,10}$', symbol):
        return {'error': 'valid symbol required (e.g. BTC, ETH)'}
    ck = f'crypto_price:{symbol}'
    cached = cache_get(ck, 30)
    if cached is not None:
        return cached
    pair = f'{symbol}USDT'
    try:
        r = req.get('https://api.binance.com/api/v3/ticker/24hr',
                    params={'symbol': pair}, timeout=8)
        if r.status_code != 200:
            return {'error': f'symbol {symbol} not found on binance'}
        j = r.json()
        out = {
            'symbol': symbol,
            'price_usd': float(j.get('lastPrice', 0)),
            'change_24h_pct': float(j.get('priceChangePercent', 0)),
            'volume_24h': float(j.get('volume', 0)),
        }
        cache_set(ck, out)
        return out
    except Exception as e:
        return {'error': f'price fetch failed: {e}'}


def _search_entries(entries: list, query: str, fields: list[str], limit: int = 5) -> list:
    q = query.lower()
    matches = []
    for e in entries:
        blob = ' '.join(str(e.get(f, '')) for f in fields).lower()
        if q in blob:
            matches.append(e)
            if len(matches) >= limit:
                break
    return matches


def tool_search_contraband(args: dict) -> dict:
    query = (args.get('query') or '').strip()
    category = (args.get('category') or '').strip().lower()
    if not query:
        return {'error': 'query required'}
    data = _load_json('contraband.json') or {}
    cats = data.get('categories', []) if isinstance(data, dict) else []
    results = []
    for cat in cats:
        cat_name = (cat.get('name') or '').lower()
        if category and category not in cat_name and category not in (cat.get('slug') or '').lower():
            continue
        for sub in cat.get('subcategories', []) or []:
            for item in sub.get('items', []) or []:
                blob = f"{item.get('title','')} {item.get('description','')} {item.get('url','')}".lower()
                if query.lower() in blob:
                    results.append({
                        'title': item.get('title'),
                        'url': item.get('url'),
                        'description': (item.get('description') or '')[:200],
                        'category': cat.get('name'),
                        'subcategory': sub.get('name'),
                    })
                    if len(results) >= 5:
                        return {'results': results, 'count': len(results)}
    return {'results': results, 'count': len(results)}


def tool_search_awesomelist(args: dict) -> dict:
    query = (args.get('query') or '').strip().lower()
    if not query:
        return {'error': 'query required'}
    index = _load_json('awesomelist_index.json') or {}
    sectors = index.get('sectors', []) if isinstance(index, dict) else []
    results = []
    # Search sector names first
    for sec in sectors:
        name = (sec.get('name') or '').lower()
        desc = (sec.get('description') or '').lower()
        if query in name or query in desc:
            results.append({
                'type': 'sector',
                'name': sec.get('name'),
                'slug': sec.get('slug'),
                'count': sec.get('count', 0),
                'url': f"/recon/awesomelist?sector={sec.get('slug')}",
            })
    # Search inside individual sector files for entry matches
    awesome_dir = DATA_DIR / 'awesomelist'
    if awesome_dir.exists() and len(results) < 5:
        import os
        for fname in sorted(os.listdir(awesome_dir))[:80]:
            if len(results) >= 5:
                break
            try:
                with open(awesome_dir / fname) as f:
                    sec_data = json.load(f)
                for entry in (sec_data.get('entries') or [])[:300]:
                    blob = f"{entry.get('name','')} {entry.get('description','')}".lower()
                    if query in blob:
                        results.append({
                            'type': 'entry',
                            'name': entry.get('name'),
                            'url': entry.get('url'),
                            'description': (entry.get('description') or '')[:200],
                            'sector_file': fname,
                        })
                        if len(results) >= 5:
                            break
            except Exception:
                continue
    return {'results': results[:5], 'count': len(results)}


def tool_get_sitrep(args: dict) -> dict:
    date_arg = (args.get('date') or 'latest').strip()
    sitrep_dir = DATA_DIR / 'sitreps'
    if not sitrep_dir.exists():
        return {'error': 'No SITREPs available yet', 'sitrep': None}
    import os
    files = sorted([f for f in os.listdir(sitrep_dir) if f.endswith('.json')], reverse=True)
    if not files:
        return {'error': 'No SITREPs archived', 'sitrep': None}
    target = None
    if date_arg == 'latest':
        target = files[0]
    else:
        for f in files:
            if date_arg in f:
                target = f
                break
    if not target:
        return {'error': f'No SITREP matching "{date_arg}"', 'available': files[:5]}
    try:
        with open(sitrep_dir / target) as f:
            data = json.load(f)
        summary = data.get('summary') or data.get('content', '')[:800]
        return {
            'date': data.get('date', target.replace('.json', '')),
            'summary': summary[:1200],
            'source_count': len(data.get('sources', [])),
            'sectors': list((data.get('sectors') or {}).keys()),
            'url': '/transmissions/sitrep',
        }
    except Exception as e:
        return {'error': f'Failed to read SITREP: {e}'}


def tool_lookup_sol_domain(args: dict) -> dict:
    name = (args.get('name') or '').strip().lower().replace('.sol', '')
    if not name or not re.match(r'^[a-z0-9_-]{1,63}$', name):
        return {'error': 'invalid domain name'}
    try:
        # Bonfida SNS public API
        r = req.get(f'https://sns-api.bonfida.com/domain/lookup/{name}', timeout=8)
        if r.status_code == 200:
            j = r.json()
            return {
                'name': f'{name}.sol',
                'registered': True,
                'owner': j.get('result', {}).get('owner') if isinstance(j.get('result'), dict) else j.get('owner'),
                'raw': j,
                'solscan_url': f'https://www.sns.id/domain?domain={name}',
            }
        if r.status_code == 404:
            return {
                'name': f'{name}.sol',
                'registered': False,
                'available': True,
                'register_url': f'https://www.sns.id/search?search={name}',
            }
        return {'name': f'{name}.sol', 'status_code': r.status_code, 'raw': r.text[:400]}
    except Exception as e:
        return {'error': f'lookup failed: {e}'}


def tool_wallet_xray(args: dict) -> dict:
    address = (args.get('address') or '').strip()
    if not address or not (32 <= len(address) <= 44):
        return {'error': 'invalid Solana address'}
    # Lazy import to avoid circular
    from auth_routes import get_rpc_url
    rpc = get_rpc_url()
    out: dict = {'address': address, 'rpc': 'helius' if 'helius' in rpc else 'public'}
    try:
        # Balance
        r = req.post(rpc, json={'jsonrpc': '2.0', 'id': 1, 'method': 'getBalance',
                                'params': [address]}, timeout=10)
        r.raise_for_status()
        lamports = int(r.json().get('result', {}).get('value', 0))
        out['balance_sol'] = round(lamports / 1_000_000_000, 6)
    except Exception as e:
        return {'error': f'balance fetch failed: {e}'}
    try:
        # Token accounts count
        r = req.post(rpc, json={
            'jsonrpc': '2.0', 'id': 2, 'method': 'getTokenAccountsByOwner',
            'params': [address, {'programId': 'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA'},
                       {'encoding': 'jsonParsed'}]
        }, timeout=12)
        tokens = r.json().get('result', {}).get('value', []) or []
        non_zero = [t for t in tokens if int(t.get('account', {}).get('data', {}).get('parsed', {}).get('info', {}).get('tokenAmount', {}).get('amount', 0) or 0) > 0]
        out['token_account_count'] = len(tokens)
        out['non_zero_tokens'] = len(non_zero)
    except Exception as e:
        out['token_error'] = str(e)
    try:
        # Last 5 signatures
        r = req.post(rpc, json={
            'jsonrpc': '2.0', 'id': 3, 'method': 'getSignaturesForAddress',
            'params': [address, {'limit': 5}]
        }, timeout=10)
        sigs = r.json().get('result', []) or []
        out['recent_txs'] = [{
            'signature': s.get('signature'),
            'slot': s.get('slot'),
            'block_time': s.get('blockTime'),
            'err': bool(s.get('err')),
        } for s in sigs]
    except Exception as e:
        out['tx_error'] = str(e)
    out['solscan_url'] = f'https://solscan.io/account/{address}'
    return out


def tool_get_changelog(args: dict) -> dict:
    limit = int(args.get('limit') or 5)
    limit = max(1, min(limit, 25))
    data = _load_json('changelog.json') or {}
    entries = data.get('entries', []) if isinstance(data, dict) else (data if isinstance(data, list) else [])
    out = []
    for e in entries[:limit]:
        out.append({
            'version': e.get('version'),
            'date': e.get('date'),
            'category': e.get('category'),
            'title': e.get('title'),
            'changes': (e.get('changes') or [])[:6],
        })
    return {'entries': out, 'count': len(out), 'total_available': len(entries)}


VALID_EFFECTS = {
    'crt', 'vhs', 'glitch', 'redalert', 'red', 'invert', 'blueprint', 'typewriter',
    'gravity', 'earthquake', 'lowgravity', 'melt', 'shuffle',
    'rain', 'snow', 'fog', 'night', 'underwater',
    'dimensions', 'portal', 'retro',
    'partymode', 'ghostmode', 'quantum', 'sneak', 'hacker',
    'matrix', 'cmatrix',
}


def tool_trigger_effect(args: dict) -> dict:
    name = (args.get('name') or '').strip().lower()
    if name not in VALID_EFFECTS:
        return {'error': f'unknown effect. Valid: {sorted(VALID_EFFECTS)}', 'effect': None}
    return {
        'action': 'trigger_effect',
        'effect': name,
        'message': f'Activating {name} effect on your browser…',
    }


# ── Registration ─────────────────────────────────────────────────────────

register(Tool(
    name='search_site',
    description='Full-text search across jaeswift.xyz content (changelog, posts, navigation, homepage). Returns top 5 matches with snippets.',
    params={
        'type': 'object',
        'properties': {'query': {'type': 'string', 'description': 'search query'}},
        'required': ['query'],
    },
    handler=tool_search_site, tier='anonymous', rate_limit='20/min',
))

register(Tool(
    name='get_sol_price',
    description='Get live Solana (SOL) price and 24h change from Binance.',
    params={'type': 'object', 'properties': {}, 'required': []},
    handler=tool_get_sol_price, tier='anonymous', rate_limit='60/min',
))

register(Tool(
    name='get_crypto_price',
    description='Get live USD price and 24h change for any crypto symbol (BTC, ETH, etc) from Binance.',
    params={
        'type': 'object',
        'properties': {'symbol': {'type': 'string', 'description': 'ticker symbol e.g. BTC, ETH, DOGE'}},
        'required': ['symbol'],
    },
    handler=tool_get_crypto_price, tier='anonymous', rate_limit='60/min',
))

register(Tool(
    name='search_contraband',
    description='Search the CONTRABAND archive of 16k+ curated resources across 24 categories (software, privacy, movies, books, games, AI, etc.). Top 5 matches.',
    params={
        'type': 'object',
        'properties': {
            'query': {'type': 'string', 'description': 'search query'},
            'category': {'type': 'string', 'description': 'optional category filter (e.g. privacy, software, ai)'},
        },
        'required': ['query'],
    },
    handler=tool_search_contraband, tier='anonymous', rate_limit='20/min',
))

register(Tool(
    name='search_awesomelist',
    description='Search 28 curated awesome-list sectors with 135k+ entries (development, datascience, security, etc.). Top 5 matches.',
    params={
        'type': 'object',
        'properties': {'query': {'type': 'string', 'description': 'search query'}},
        'required': ['query'],
    },
    handler=tool_search_awesomelist, tier='anonymous', rate_limit='20/min',
))

register(Tool(
    name='get_sitrep',
    description='Get the latest (or a specific date) SITREP daily AI intelligence briefing covering tech, cybersecurity, and crypto.',
    params={
        'type': 'object',
        'properties': {'date': {'type': 'string', 'description': 'date (YYYY-MM-DD) or "latest"', 'default': 'latest'}},
        'required': [],
    },
    handler=tool_get_sitrep, tier='anonymous', rate_limit='20/min',
))

register(Tool(
    name='lookup_sol_domain',
    description='Check if a .sol domain is registered or available (via Bonfida SNS). Returns owner if registered.',
    params={
        'type': 'object',
        'properties': {'name': {'type': 'string', 'description': 'domain name without .sol suffix'}},
        'required': ['name'],
    },
    handler=tool_lookup_sol_domain, tier='anonymous', rate_limit='30/min',
))

register(Tool(
    name='wallet_xray',
    description='Scan any Solana wallet address: SOL balance, token account count, recent transactions.',
    params={
        'type': 'object',
        'properties': {'address': {'type': 'string', 'description': 'Solana wallet pubkey (base58)'}},
        'required': ['address'],
    },
    handler=tool_wallet_xray, tier='anonymous', rate_limit='10/min', timeout=20,
))

register(Tool(
    name='get_changelog',
    description='Get the last N changelog entries from jaeswift.xyz (what has been built/fixed recently).',
    params={
        'type': 'object',
        'properties': {'limit': {'type': 'integer', 'description': 'number of entries (1-25)', 'default': 5}},
        'required': [],
    },
    handler=tool_get_changelog, tier='anonymous', rate_limit='30/min',
))

register(Tool(
    name='trigger_effect',
    description='Activate a sitewide visual effect on the user\'s browser (crt, vhs, glitch, redalert, matrix, rain, snow, partymode, etc.). Returns an action payload the frontend will execute.',
    params={
        'type': 'object',
        'properties': {'name': {'type': 'string', 'description': 'effect name (e.g. crt, rain, partymode)'}},
        'required': ['name'],
    },
    handler=tool_trigger_effect, tier='anonymous', rate_limit='30/min',
))


# ── Public API ───────────────────────────────────────────────────────────

def get_allowed_tools(user_tier: str) -> list[Tool]:
    from agent_tiers import tier_allows
    return [t for t in TOOLS.values() if tier_allows(user_tier, t.tier)]


def openai_tool_schemas(tools: list[Tool]) -> list[dict]:
    return [{
        'type': 'function',
        'function': {
            'name': t.name,
            'description': t.description,
            'parameters': t.params,
        },
    } for t in tools]


# ── Extended tool registry (Phase 3 — 27 additional tools) ──────────────
try:
    import agent_tools_extended  # noqa: F401  registers on import
except Exception as _ext_err:
    import sys
    print(f'[agent_tools] extended tools failed to load: {_ext_err}', file=sys.stderr)
