"""Proposal Intake Pipeline.

Accepts raw text (articles, URLs, notes) and automatically generates
IETF draft proposals cross-referenced with existing gaps.

Usage:
    from ietf_analyzer.proposal_intake import ProposalIntake
    intake = ProposalIntake(config, db)
    proposals = intake.process("paste article text or URLs here")
"""

from __future__ import annotations

import hashlib
import ipaddress
import json
import re
import socket
from datetime import datetime, timezone
from pathlib import Path
from urllib.parse import urlparse

from dotenv import load_dotenv
# Load .env from project root (same pattern as analyzer.py)
load_dotenv(Path(__file__).resolve().parent.parent.parent / ".env")
load_dotenv()

import anthropic
import httpx
from rich.console import Console

from .config import Config
from .db import Database

console = Console()

INTAKE_SYSTEM_PROMPT = """\
You are an IETF standards expert who identifies potential Internet-Draft proposals \
from research papers, articles, and technical discussions.

You will receive:
1. A list of existing GAPS in IETF AI/agent standards coverage
2. Input text (articles, papers, notes, or summaries)

Your job: identify concrete IETF Internet-Draft proposals that could address one or \
more of the gaps, inspired by the input material.

For EACH proposal you identify, output a JSON object with these fields:
- title: descriptive title for the proposed draft
- slug: URL-friendly identifier (lowercase, hyphens, e.g. "capability-security-policies")
- status: always "idea" for new proposals
- description: 2-3 sentence summary of what this draft would standardize
- content_md: full proposal outline in markdown (kramdown-compatible), including:
  - Problem Statement (what gap does this address, why is it needed)
  - Scope (what the draft defines and what's out of scope)
  - Key Concepts (core technical ideas from the source material)
  - Proposed Approach (wire formats, protocols, data models — be specific)
  - Integration Points (how this fits with WIMSE, ECT, MCP, A2A, OAuth, etc.)
  - Security Considerations
  - Open Questions
  - References (to the source material and relevant existing standards)
- source_paper: title of the source paper/article
- source_url: URL if available, empty string otherwise
- intended_wg: suggested IETF working group (e.g. "SECDISPATCH", "WIMSE", "new WG")
- draft_name: suggested draft filename (e.g. "draft-nennemann-ai-agent-capability-policies-00")
- gap_ids: array of gap IDs (integers) that this proposal addresses

Guidelines:
- Be SPECIFIC about wire formats, protocol messages, data structures
- Include JSON examples in the content_md where applicable
- Cross-reference with existing IETF work (WIMSE, ECT, MCP, A2A, GNAP, OAuth)
- One paper/article might produce MULTIPLE proposals — don't merge distinct ideas
- Don't create proposals that duplicate existing IETF drafts
- The content_md should be substantial (500+ words) — this is a working document
- Use kramdown-compatible markdown (YAML front matter is optional, not required in content)

Output ONLY a JSON array of proposal objects. No other text."""

GAPS_TEMPLATE = """\
## Existing Gaps in IETF AI/Agent Standards

{gap_list}

## Input Material

{input_text}

---

Analyze the input material against the gaps above. Generate IETF draft proposals \
as a JSON array. Each proposal should be a concrete, actionable draft idea that \
addresses one or more gaps."""


def _prompt_hash(text: str) -> str:
    return hashlib.sha256(text.encode()).hexdigest()[:16]


class ProposalIntake:
    """Process raw text/URLs into structured IETF draft proposals."""

    def __init__(self, config: Config, db: Database):
        self.config = config
        self.db = db
        try:
            self.client = anthropic.Anthropic()
        except Exception:
            console.print(
                "[red bold]No Anthropic API key found.[/]\n"
                "Set ANTHROPIC_API_KEY environment variable or add to .env"
            )
            raise SystemExit(1)

    @staticmethod
    def _is_private_url(url: str) -> bool:
        """Block requests to private/loopback IP ranges (SSRF protection)."""
        try:
            hostname = urlparse(url).hostname
            if not hostname:
                return True
            addr = socket.getaddrinfo(hostname, None, socket.AF_UNSPEC, socket.SOCK_STREAM)
            for _, _, _, _, sockaddr in addr:
                ip = ipaddress.ip_address(sockaddr[0])
                if ip.is_private or ip.is_loopback or ip.is_link_local or ip.is_reserved:
                    return True
        except (socket.gaierror, ValueError, OSError):
            return True
        return False

    def fetch_url(self, url: str) -> str:
        """Fetch a URL and return its text content (best-effort)."""
        if self._is_private_url(url):
            return f"[Blocked: private/internal URL {url}]"
        try:
            resp = httpx.get(url, follow_redirects=True, timeout=30,
                             headers={"User-Agent": "IETF-Draft-Analyzer/1.0"})
            resp.raise_for_status()
            content_type = resp.headers.get("content-type", "")
            if "html" in content_type:
                # Simple HTML → text extraction
                text = resp.text
                # Strip tags
                text = re.sub(r'<script[^>]*>.*?</script>', '', text, flags=re.DOTALL)
                text = re.sub(r'<style[^>]*>.*?</style>', '', text, flags=re.DOTALL)
                text = re.sub(r'<[^>]+>', ' ', text)
                text = re.sub(r'\s+', ' ', text).strip()
                return text[:50000]  # Cap at 50k chars
            elif "pdf" in content_type:
                return f"[PDF at {url} — paste the text content directly for better results]"
            else:
                return resp.text[:50000]
        except Exception as e:
            return f"[Failed to fetch {url}: {e}]"

    def extract_urls(self, text: str) -> list[str]:
        """Extract URLs from text."""
        return re.findall(r'https?://[^\s<>"\')\]]+', text)

    def prepare_input(self, raw_input: str) -> str:
        """Fetch any URLs in the input and combine with the raw text."""
        urls = self.extract_urls(raw_input)
        parts = []

        if urls:
            # Fetch each URL
            for url in urls:
                console.print(f"  Fetching [blue]{url}[/]...")
                content = self.fetch_url(url)
                parts.append(f"### Source: {url}\n\n{content}\n")

            # Also include any non-URL text
            non_url_text = raw_input
            for url in urls:
                non_url_text = non_url_text.replace(url, "").strip()
            if non_url_text:
                parts.append(f"### Additional notes\n\n{non_url_text}\n")
        else:
            parts.append(raw_input)

        return "\n---\n".join(parts)

    def build_prompt(self, input_text: str) -> str:
        """Build the full prompt with gaps context."""
        gaps = self.db.all_gaps()
        gap_lines = []
        for g in gaps:
            gap_lines.append(
                f"- **Gap #{g['id']}** [{g['severity'].upper()}] ({g['category']}): "
                f"{g['topic']} — {g['description']}"
            )
        gap_list = "\n".join(gap_lines)
        return GAPS_TEMPLATE.format(gap_list=gap_list, input_text=input_text)

    def call_claude(self, prompt: str, cheap: bool = False) -> tuple[str, dict]:
        """Call Claude with the intake prompt.

        Returns (response_text, usage_info) where usage_info has
        keys: model, input_tokens, output_tokens, cost_usd.
        """
        model = self.config.claude_model_cheap if cheap else self.config.claude_model
        console.print(f"  Calling Claude ({model})...")
        resp = self.client.messages.create(
            model=model,
            max_tokens=16000,
            system=INTAKE_SYSTEM_PROMPT,
            messages=[{"role": "user", "content": prompt}],
        )
        text = resp.content[0].text.strip()
        in_tok = resp.usage.input_tokens
        out_tok = resp.usage.output_tokens
        # Haiku: ~$0.80/M in, $4/M out; Sonnet: ~$3/M in, $15/M out
        if "haiku" in model:
            cost = (in_tok * 0.8 + out_tok * 4) / 1_000_000
        else:
            cost = (in_tok * 3 + out_tok * 15) / 1_000_000
        console.print(
            f"  Tokens: {in_tok:,} in / {out_tok:,} out (~${cost:.3f})"
        )
        usage = {
            "model": model,
            "input_tokens": in_tok,
            "output_tokens": out_tok,
            "cost_usd": round(cost, 4),
        }
        return text, usage

    def parse_proposals(self, response: str) -> list[dict]:
        """Parse Claude's JSON response into proposal dicts."""
        # Strip markdown fences if present
        text = response.strip()
        if text.startswith("```"):
            text = text.split("\n", 1)[1]
            if text.rstrip().endswith("```"):
                text = text.rstrip()[:-3]
        text = text.strip()

        try:
            proposals = json.loads(text)
            if isinstance(proposals, dict):
                proposals = [proposals]
            return proposals
        except json.JSONDecodeError as e:
            console.print(f"[red]Failed to parse JSON: {e}[/]")
            console.print(f"[dim]Response was: {text[:500]}...[/]")
            return []

    def store_proposals(self, proposals: list[dict]) -> list[int]:
        """Store parsed proposals in the database. Returns list of proposal IDs."""
        ids = []
        for p in proposals:
            # Ensure gap_ids are ints
            gap_ids = [int(g) for g in p.get("gap_ids", []) if str(g).isdigit()]
            proposal = {
                "title": p.get("title", "Untitled"),
                "slug": p.get("slug", f"proposal-{_prompt_hash(p.get('title', ''))}"),
                "status": p.get("status", "idea"),
                "description": p.get("description", ""),
                "content_md": p.get("content_md", ""),
                "source_paper": p.get("source_paper", ""),
                "source_url": p.get("source_url", ""),
                "intended_wg": p.get("intended_wg", ""),
                "draft_name": p.get("draft_name", ""),
                "gap_ids": gap_ids,
            }
            try:
                pid = self.db.upsert_proposal(proposal)
                ids.append(pid)
                console.print(
                    f"  [green]✓[/] Stored: {proposal['title']} "
                    f"(#{pid}, {len(gap_ids)} gaps linked)"
                )
            except Exception as e:
                console.print(f"  [red]✗[/] Failed to store '{proposal['title']}': {e}")
        return ids

    def process(
        self, raw_input: str, dry_run: bool = False, cheap: bool = False
    ) -> tuple[list[dict], dict]:
        """Full pipeline: input → fetch URLs → build prompt → Claude → store.

        Args:
            raw_input: text with URLs, article content, or notes
            dry_run: if True, return proposals without storing
            cheap: if True, use Haiku instead of Sonnet

        Returns:
            (list of proposal dicts with 'id' set if stored, usage_info dict)
        """
        console.print("[bold]Proposal Intake Pipeline[/]")
        console.print(f"  Input: {len(raw_input)} chars, {len(self.extract_urls(raw_input))} URLs")

        # Step 1: Prepare input (fetch URLs)
        input_text = self.prepare_input(raw_input)
        console.print(f"  Prepared: {len(input_text)} chars")

        # Step 2: Build prompt with gaps
        prompt = self.build_prompt(input_text)

        # Step 3: Call Claude
        response, usage = self.call_claude(prompt, cheap=cheap)

        # Step 4: Parse
        proposals = self.parse_proposals(response)
        console.print(f"  Parsed: {len(proposals)} proposal(s)")

        if not proposals:
            return [], usage

        # Step 5: Store (unless dry run)
        if not dry_run:
            ids = self.store_proposals(proposals)
            for p, pid in zip(proposals, ids):
                p["id"] = pid
        else:
            console.print("  [yellow]Dry run — not stored[/]")

        return proposals, usage
