#!/usr/bin/env python3
"""
Nostur -> Gitea Ingress Bridge MVP
Reads DMs from Nostr and creates Gitea issues/comments
"""
import asyncio
import json
import os
import sys
import time
from datetime import datetime, timedelta
from urllib.request import Request, urlopen

# nostr_sdk imports
try:
    from nostr_sdk import Keys, Client, Filter, Kind, NostrSigner, Timestamp, RelayUrl, PublicKey
except ImportError as e:
    print(f"[ERROR] nostr_sdk import failed: {e}")
    sys.exit(1)

# Configuration
GITEA = "https://forge.alexanderwhitestone.com"
RELAY_URL = "ws://localhost:2929"  # Local relay
POLL_INTERVAL = 60  # Seconds between polls
ALLOWED_PUBKEYS = []  # Will load from keystore
_GITEA_TOKEN = None

# Load credentials
def load_keystore():
    with open("/root/nostr-relay/keystore.json") as f:
        return json.load(f)

def load_gitea_token():
    global _GITEA_TOKEN
    if _GITEA_TOKEN:
        return _GITEA_TOKEN
    for path in ["/root/.gitea_token", os.path.expanduser("~/.gitea_token")]:
        try:
            with open(path) as f:
                _GITEA_TOKEN = f.read().strip()
            if _GITEA_TOKEN:
                return _GITEA_TOKEN
        except FileNotFoundError:
            pass
    return None

def load_allowed_pubkeys():
    """Load sovereign operator pubkeys that can create work"""
    keystore = load_keystore()
    allowed = []
    # Alexander's pubkey is the primary operator
    if "alexander" in keystore:
        allowed.append(keystore["alexander"].get("pubkey", ""))
        allowed.append(keystore["alexander"].get("hex_public", ""))
    return [p for p in allowed if p]

# Gitea API helpers
def gitea_post(path, data):
    token = load_gitea_token()
    if not token:
        raise RuntimeError("Gitea token not available")
    headers = {"Authorization": f"token {token}", "Content-Type": "application/json"}
    body = json.dumps(data).encode()
    req = Request(f"{GITEA}/api/v1{path}", data=body, headers=headers, method="POST")
    with urlopen(req, timeout=15) as resp:
        return json.loads(resp.read().decode())

def gitea_get(path):
    token = load_gitea_token()
    if not token:
        raise RuntimeError("Gitea token not available")
    headers = {"Authorization": f"token {token}"}
    req = Request(f"{GITEA}/api/v1{path}", headers=headers)
    with urlopen(req, timeout=15) as resp:
        return json.loads(resp.read().decode())

def create_issue(repo, title, body, assignees=None):
    """Create a Gitea issue from DM content"""
    data = {
        "title": f"[NOSTR] {title}",
        "body": f"**Ingress via Nostr DM**\n\n{body}\n\n---\n*Created by Nostur→Gitea Bridge MVP*"
    }
    if assignees:
        data["assignees"] = assignees
    return gitea_post(f"/repos/{repo}/issues", data)

def add_comment(repo, issue_num, body):
    """Add comment to existing issue"""
    return gitea_post(f"/repos/{repo}/issues/{issue_num}/comments", {
        "body": f"**Nostr DM Update**\n\n{body}\n\n---\n*Posted by Bridge MVP*"
    })

def get_open_issues(repo, label=None):
    """Get open issues for status summary"""
    path = f"/repos/{repo}/issues?state=open&limit=20"
    if label:
        path += f"&labels={label}"
    return gitea_get(path)

# DM Content Processing
def parse_dm_command(content):
    """
    Parse DM content for commands:
    - 'status' -> return queue summary
    - 'create <repo> <title>' -> create issue
    - 'comment <repo> #<num> <text>' -> add comment
    """
    content = content.strip()
    lines = content.split('\n')
    first_line = lines[0].strip().lower()
    
    if first_line == 'status' or first_line.startswith('status'):
        return {'cmd': 'status', 'repo': 'Timmy_Foundation/the-nexus'}
    
    if first_line.startswith('create '):
        parts = content[7:].split(' ', 1)  # Skip 'create '
        if len(parts) >= 2:
            repo = parts[0] if '/' in parts[0] else f"Timmy_Foundation/{parts[0]}"
            return {'cmd': 'create', 'repo': repo, 'title': parts[1], 'body': '\n'.join(lines[1:]) if len(lines) > 1 else ''}
    
    if first_line.startswith('comment '):
        parts = content[8:].split(' ', 2)  # Skip 'comment '
        if len(parts) >= 3:
            repo = parts[0] if '/' in parts[0] else f"Timmy_Foundation/{parts[0]}"
            issue_ref = parts[1]  # e.g., #123
            if issue_ref.startswith('#'):
                issue_num = issue_ref[1:]
                return {'cmd': 'comment', 'repo': repo, 'issue': issue_num, 'body': parts[2]}
    
    return {'cmd': 'unknown', 'raw': content}

def execute_command(cmd, author_npub):
    """Execute parsed command and return result"""
    try:
        if cmd['cmd'] == 'status':
            issues = get_open_issues(cmd['repo'])
            priority = [i for i in issues if not i.get('assignee')]
            blockers = [i for i in issues if any(l['name'] == 'blocker' for l in i.get('labels', []))]
            
            summary = f"📊 **Queue Status for {cmd['repo']}**\n\n"
            summary += f"Open issues: {len(issues)}\n"
            summary += f"Unassigned (priority): {len(priority)}\n"
            summary += f"Blockers: {len(blockers)}\n\n"
            
            if priority[:3]:
                summary += "**Top Priority (unassigned):**\n"
                for i in priority[:3]:
                    summary += f"- #{i['number']}: {i['title'][:50]}...\n"
            
            return {'success': True, 'message': summary, 'action': 'status'}
        
        elif cmd['cmd'] == 'create':
            result = create_issue(cmd['repo'], cmd['title'], cmd['body'])
            url = result.get('html_url', f"{GITEA}/{cmd['repo']}/issues/{result['number']}")
            return {
                'success': True, 
                'message': f"✅ Created issue #{result['number']}: {result['title']}\n🔗 {url}",
                'action': 'create',
                'issue_num': result['number'],
                'url': url
            }
        
        elif cmd['cmd'] == 'comment':
            result = add_comment(cmd['repo'], cmd['issue'], cmd['body'])
            return {
                'success': True,
                'message': f"✅ Added comment to {cmd['repo']}#{cmd['issue']}",
                'action': 'comment'
            }
        
        else:
            return {'success': False, 'message': f"Unknown command. Try: status, create <repo> <title>, comment <repo> #<num> <text>"}
    
    except Exception as e:
        return {'success': False, 'message': f"Error: {str(e)}"}

# Nostr DM processing
async def poll_dms(client, signer, since_ts):
    """Poll for DMs and process commands"""
    keystore = load_keystore()
    allowed_pubkeys = load_allowed_pubkeys()
    
    # Note: relay29 restricts kinds, kind 4 may be blocked
    filter_dm = Filter().kind(Kind(4)).since(since_ts)
    
    events_processed = 0
    commands_executed = 0
    
    try:
        events = await client.fetch_events(filter_dm, timedelta(seconds=5))
        
        for event in events:
            author = event.author().to_hex()
            author_npub = event.author().to_bech32()
            
            # Verify sovereign identity
            if author not in allowed_pubkeys:
                print(f"  [SKIP] Event from unauthorized pubkey: {author[:16]}...")
                continue
            
            events_processed += 1
            print(f"  [DM] Event {event.id().to_hex()[:16]}... from {author_npub[:20]}...")
            
            # Decrypt content (requires NIP-44 or NIP-04 decryption)
            try:
                # Try to decrypt using signer's decrypt method
                # Note: This is for NIP-04, NIP-44 may need different handling
                decrypted = signer.decrypt(author, event.content())
                content = decrypted
                print(f"       Content preview: {content[:80]}...")
                
                # Parse and execute command
                cmd = parse_dm_command(content)
                if cmd['cmd'] != 'unknown':
                    result = execute_command(cmd, author_npub)
                    commands_executed += 1
                    print(f"       ✅ {result.get('action', 'unknown')}: {result.get('message', '')[:60]}...")
                    
                    # Send acknowledgement DM back
                    try:
                        reply_content = f"ACK: {result.get('message', 'Command processed')[:200]}"
                        # Build and send DM reply
                        recipient = PublicKey.parse(author)
                        # Note: Sending DMs requires proper event construction
                        # This is a placeholder - actual send needs NIP-04/NIP-44 event building
                        print(f"       [ACK] Would send: {reply_content[:60]}...")
                    except Exception as ack_err:
                        print(f"       [ACK ERROR] Failed to send acknowledgement: {ack_err}")
                else:
                    print(f"       [PARSE] Unrecognized command format")
                    
            except Exception as e:
                print(f"       [ERROR] Failed to process: {e}")
        
        return events_processed, commands_executed
        
    except Exception as e:
        print(f"[BRIDGE] DM fetch issue (may be relay restriction): {e}")
        return 0, 0

async def run_bridge_loop():
    """Main bridge loop - runs continuously"""
    keystore = load_keystore()
    
    # Initialize Allegro's keys with NostrSigner
    allegro_hex = keystore["allegro"]["hex_secret"]
    keys = Keys.parse(allegro_hex)
    signer = NostrSigner.keys(keys)
    
    # Create client with signer
    client = Client(signer)
    relay_url = RelayUrl.parse(RELAY_URL)
    await client.add_relay(relay_url)
    await client.connect()
    
    print(f"[BRIDGE] Connected to relay as {keystore['allegro']['npub'][:32]}...")
    print(f"[BRIDGE] Monitoring DMs from authorized pubkeys: {len(load_allowed_pubkeys())}")
    print(f"[BRIDGE] Poll interval: {POLL_INTERVAL}s")
    print("="*60)
    
    last_check = Timestamp.now()
    
    try:
        while True:
            print(f"\n[{datetime.utcnow().strftime('%H:%M:%S')}] Polling for DMs...")
            events, commands = await poll_dms(client, signer, last_check)
            last_check = Timestamp.now()
            
            if events > 0 or commands > 0:
                print(f"       Processed: {events} events, {commands} commands")
            else:
                print(f"       No new DMs")
            
            await asyncio.sleep(POLL_INTERVAL)
            
    except KeyboardInterrupt:
        print("\n[BRIDGE] Shutting down...")
    finally:
        await client.disconnect()

def main():
    print("="*60)
    print("NOSTUR → GITEA BRIDGE MVP")
    print("Continuous DM → Issue Bridge Service")
    print("="*60)
    
    # Verify keystore
    keystore = load_keystore()
    print(f"[INIT] Keystore loaded: {len(keystore)} identities")
    print(f"[INIT] Allegro npub: {keystore['allegro']['npub'][:32]}...")
    
    # Verify Gitea API
    token = load_gitea_token()
    if not token:
        print("[ERROR] Gitea token not found")
        sys.exit(1)
    print(f"[INIT] Gitea token loaded: {token[:8]}...")
    
    # Load allowed pubkeys
    allowed = load_allowed_pubkeys()
    print(f"[INIT] Allowed operators: {len(allowed)}")
    for pk in allowed:
        print(f"       - {pk[:32]}...")
    
    # Run bridge loop
    try:
        asyncio.run(run_bridge_loop())
    except Exception as e:
        print(f"\n[ERROR] Bridge crashed: {e}")
        import traceback
        traceback.print_exc()
        sys.exit(1)

if __name__ == "__main__":
    main()
