# Familiar - Self-Hosted AI Companion Platform
# Copyright (c) 2026 George Scott Foley
# ORCID: 0009-0006-4957-0540
# Email: Georgescottfoley@proton.me
# Licensed under the MIT License - see LICENSE file for details

"""
OSINT Skill

Open-source intelligence gathering tools: target authorization, email harvesting,
subdomain enumeration, DNS, WHOIS, certificate transparency, and breach data.

Constitution Article 8 — "The Public Record Is Not a License":
ALL external reconnaissance requires explicit per-target authorization via
confirm_osint_target() before any other OSINT tool will execute.

Setup (optional API integrations):
    SHODAN_API_KEY=your-shodan-key
    HIBP_API_KEY=your-have-i-been-pwned-key    # https://haveibeenpwned.com/API/Key
    THEHARVESTER_PATH=/usr/bin/theHarvester    # auto-detected if in PATH
    AMASS_PATH=/usr/bin/amass                  # auto-detected if in PATH
"""

import json
import logging
import os
import shutil
import socket
import subprocess
from datetime import datetime, timezone

import httpx

logger = logging.getLogger(__name__)

REQUEST_TIMEOUT = 30
SUBPROCESS_TIMEOUT = 120


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------


def _tool_available(name: str, env_key: str | None = None) -> str | None:
    """Return path to tool or None if unavailable."""
    if env_key:
        custom = os.environ.get(env_key, "")
        if custom and os.path.isfile(custom):
            return custom
    return shutil.which(name)


def _run(cmd: list, timeout: int = SUBPROCESS_TIMEOUT) -> tuple[int, str, str]:
    try:
        result = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout)
        return result.returncode, result.stdout, result.stderr
    except subprocess.TimeoutExpired:
        return -1, "", f"Timed out after {timeout}s"
    except Exception as e:
        return -1, "", str(e)


# ---------------------------------------------------------------------------
# Tool handlers
# ---------------------------------------------------------------------------


def confirm_osint_target(data: dict) -> str:
    """
    Establish explicit authorization for OSINT reconnaissance against a target.

    This is required before any other OSINT tool can run against an external target.
    Records the justification and scope to the authorization cache (1-hour TTL).
    """
    target = (data.get("target") or "").strip()
    justification = (data.get("justification") or "").strip()
    scope = (data.get("scope") or "").strip()

    if not target:
        return "target is required (domain, IP, email, or organization name)."
    if not justification:
        return (
            "justification is required. Explain the legitimate purpose "
            "(e.g., 'Due diligence research for business partnership', "
            "'Authorized penetration test — SOW ref #1234')."
        )
    if not scope:
        return (
            "scope is required. Describe what's authorized "
            "(e.g., 'Domain footprint only, no personal data', "
            "'Full reconnaissance including subdomains and email harvest')."
        )

    try:
        from familiar.skills.osint.auth import authorize_target

        record = authorize_target(target, justification, scope)
        return (
            f"OSINT authorization granted for '{target}'.\n"
            f"  Justification: {justification}\n"
            f"  Scope: {scope}\n"
            f"  Valid until: {record['expires_at_iso']} (1 hour)\n"
            f"  Authorization ID: {record['id']}\n\n"
            f"You may now use OSINT tools against this target within the authorized scope."
        )
    except Exception as e:
        logger.error("confirm_osint_target failed: %s", e)
        return f"Failed to record authorization: {e}"


def harvest_emails(data: dict) -> str:
    """Harvest email addresses for a domain using theHarvester."""
    target = (data.get("domain") or data.get("target") or "").strip()
    if not target:
        return "domain is required."

    harvester = _tool_available("theHarvester", "THEHARVESTER_PATH")
    if not harvester:
        return (
            "theHarvester is not installed. Install it with: "
            "sudo dnf install theHarvester  (or pip install theHarvester)"
        )

    sources = data.get("sources", "google,bing,yahoo,duckduckgo")
    limit = min(int(data.get("limit", 100)), 500)

    cmd = [harvester, "-d", target, "-b", sources, "-l", str(limit), "-f", "/tmp/harvester_out"]
    rc, stdout, stderr = _run(cmd, timeout=SUBPROCESS_TIMEOUT)

    if rc == -1:
        return f"theHarvester failed: {stderr}"

    # Parse emails from output
    emails: list[str] = []
    hosts: list[str] = []
    in_emails = in_hosts = False

    for line in stdout.splitlines():
        line = line.strip()
        if "[*] Emails found:" in line or "Emails found" in line:
            in_emails = True
            in_hosts = False
            continue
        if "[*] Hosts found:" in line or "Hosts found" in line:
            in_hosts = True
            in_emails = False
            continue
        if line.startswith("[*]") or line.startswith("---"):
            in_emails = in_hosts = False
            continue
        if in_emails and "@" in line:
            emails.append(line)
        if in_hosts and line and not line.startswith("["):
            hosts.append(line)

    lines = [f"Email harvest for {target}:"]
    lines.append(f"  Emails found: {len(emails)}")
    for email in sorted(set(emails))[:50]:
        lines.append(f"    {email}")
    if len(emails) > 50:
        lines.append(f"    ... and {len(emails) - 50} more")

    if hosts:
        lines.append(f"\n  Hosts found: {len(hosts)}")
        for host in sorted(set(hosts))[:20]:
            lines.append(f"    {host}")

    return "\n".join(lines)


def enumerate_subdomains(data: dict) -> str:
    """Enumerate subdomains for a domain using Amass (passive mode by default)."""
    target = (data.get("domain") or data.get("target") or "").strip()
    if not target:
        return "domain is required."

    amass = _tool_available("amass", "AMASS_PATH")
    if not amass:
        # Fall back to pure DNS brute-force of common subdomains if amass unavailable
        return _subdomain_dns_fallback(target, data.get("passive", True))

    passive = data.get("passive", True)
    cmd = [amass, "enum", "-d", target, "-timeout", "60"]
    if passive:
        cmd.append("-passive")

    rc, stdout, stderr = _run(cmd, timeout=SUBPROCESS_TIMEOUT)

    if rc == -1:
        return f"Amass timed out or failed: {stderr}"

    subdomains = [line.strip() for line in stdout.splitlines() if line.strip()]
    mode = "passive" if passive else "active"

    lines = [f"Subdomain enumeration for {target} ({mode}):"]
    lines.append(f"  Found: {len(subdomains)} subdomains")
    for sub in sorted(set(subdomains))[:50]:
        lines.append(f"    {sub}")
    if len(subdomains) > 50:
        lines.append(f"    ... and {len(subdomains) - 50} more")

    return "\n".join(lines)


def _subdomain_dns_fallback(target: str, passive: bool) -> str:
    """Lightweight DNS-based subdomain check when Amass is unavailable."""
    common = [
        "www",
        "mail",
        "smtp",
        "imap",
        "pop",
        "ftp",
        "ssh",
        "vpn",
        "remote",
        "api",
        "dev",
        "staging",
        "test",
        "beta",
        "admin",
        "portal",
        "app",
        "static",
        "cdn",
        "assets",
        "media",
        "img",
        "images",
        "blog",
        "shop",
        "store",
        "support",
        "help",
        "docs",
        "wiki",
        "git",
        "gitlab",
        "github",
        "jira",
        "confluence",
        "jenkins",
        "ci",
        "monitoring",
        "dashboard",
    ]
    found = []
    for prefix in common:
        hostname = f"{prefix}.{target}"
        try:
            socket.setdefaulttimeout(2)
            socket.gethostbyname(hostname)
            found.append(hostname)
        except (socket.gaierror, socket.timeout):
            pass

    lines = [
        f"Subdomain check for {target} (DNS fallback — install amass for full enumeration):",
        f"  Checked: {len(common)} common prefixes  Found: {len(found)}",
    ]
    for sub in found:
        lines.append(f"    {sub}")
    return "\n".join(lines)


def dns_enumerate(data: dict) -> str:
    """Enumerate DNS records for a domain (A, MX, NS, TXT, DKIM, SPF, DMARC)."""
    target = (data.get("domain") or data.get("target") or "").strip()
    if not target:
        return "domain is required."

    try:
        import dns.resolver  # type: ignore

        has_dnspython = True
    except ImportError:
        has_dnspython = False

    if has_dnspython:
        return _dns_enumerate_dnspython(target)
    else:
        return _dns_enumerate_dig(target)


def _dns_enumerate_dnspython(domain: str) -> str:
    import dns.resolver  # type: ignore

    lines = [f"DNS records for {domain}:"]
    record_types = ["A", "AAAA", "MX", "NS", "TXT", "SOA", "CNAME"]

    for rtype in record_types:
        try:
            answers = dns.resolver.resolve(domain, rtype, lifetime=5)
            lines.append(f"\n  {rtype}:")
            for rdata in answers:
                lines.append(f"    {rdata}")
        except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer, dns.resolver.NoNameservers):
            pass
        except Exception:
            pass

    # Check for SPF, DMARC, DKIM common selectors
    special = {
        "SPF": (domain, "TXT"),
        "DMARC": (f"_dmarc.{domain}", "TXT"),
    }
    for label, (check_domain, rtype) in special.items():
        try:
            answers = dns.resolver.resolve(check_domain, rtype, lifetime=5)
            for rdata in answers:
                txt = str(rdata)
                if "v=spf1" in txt or "v=DMARC1" in txt:
                    lines.append(f"\n  {label}:")
                    lines.append(f"    {txt}")
        except Exception:
            pass

    return "\n".join(lines)


def _dns_enumerate_dig(domain: str) -> str:
    dig = shutil.which("dig")
    if not dig:
        return "Neither dnspython nor dig is available. Install: pip install dnspython"

    lines = [f"DNS records for {domain}:"]
    for rtype in ("A", "AAAA", "MX", "NS", "TXT", "SOA"):
        rc, stdout, _ = _run([dig, "+short", rtype, domain], timeout=10)
        if rc == 0 and stdout.strip():
            lines.append(f"\n  {rtype}:")
            for rec in stdout.strip().splitlines():
                lines.append(f"    {rec}")
    return "\n".join(lines)


def whois_lookup(data: dict) -> str:
    """Perform a WHOIS lookup for a domain or IP address."""
    target = (data.get("target") or data.get("domain") or data.get("ip") or "").strip()
    if not target:
        return "target is required (domain or IP)."

    # Try python-whois first
    try:
        import whois as pythonwhois  # type: ignore

        w = pythonwhois.whois(target)
        lines = [f"WHOIS for {target}:"]
        fields = [
            ("Domain Name", "domain_name"),
            ("Registrar", "registrar"),
            ("Created", "creation_date"),
            ("Updated", "updated_date"),
            ("Expires", "expiration_date"),
            ("Name Servers", "name_servers"),
            ("Status", "status"),
            ("Org", "org"),
            ("Country", "country"),
            ("Emails", "emails"),
        ]
        for label, key in fields:
            val = getattr(w, key, None)
            if val:
                if isinstance(val, list):
                    val = ", ".join(str(v) for v in val[:3])
                lines.append(f"  {label}: {val}")
        return "\n".join(lines)

    except ImportError:
        pass
    except Exception as e:
        logger.debug("python-whois failed for %s: %s", target, e)

    # Fall back to whois CLI
    whois_bin = shutil.which("whois")
    if not whois_bin:
        return (
            "Neither python-whois nor the whois CLI is available. Install: pip install python-whois"
        )

    rc, stdout, stderr = _run([whois_bin, target], timeout=30)
    if rc != 0 or not stdout.strip():
        return f"WHOIS lookup failed for {target}: {stderr[:200]}"

    # Filter to most useful lines
    keep = []
    interesting = (
        "registrar",
        "created",
        "updated",
        "expir",
        "name server",
        "org",
        "country",
        "status",
        "registrant",
    )
    for line in stdout.splitlines()[:60]:
        low = line.lower()
        if any(kw in low for kw in interesting):
            keep.append(line.strip())

    return f"WHOIS for {target}:\n" + ("\n".join(keep[:30]) or stdout[:1500])


def ip_geolocation(data: dict) -> str:
    """Geolocate an IP address using the free ip-api.com service."""
    ip = (data.get("ip") or data.get("target") or "").strip()
    if not ip:
        return "ip is required."

    try:
        resp = httpx.get(
            f"http://ip-api.com/json/{ip}",
            params={
                "fields": "status,message,country,regionName,city,zip,lat,lon,isp,org,as,query"
            },
            timeout=REQUEST_TIMEOUT,
        )
        if resp.status_code != 200:
            return f"Geolocation request failed (HTTP {resp.status_code})"

        d = resp.json()
        if d.get("status") != "success":
            return f"Geolocation failed: {d.get('message', 'unknown error')}"

        lines = [
            f"Geolocation for {d.get('query', ip)}:",
            f"  Location:  {d.get('city', '?')}, {d.get('regionName', '?')}, "
            f"{d.get('country', '?')} {d.get('zip', '')}",
            f"  Coords:    {d.get('lat', '?')}, {d.get('lon', '?')}",
            f"  ISP:       {d.get('isp', '?')}",
            f"  Org:       {d.get('org', '?')}",
            f"  ASN:       {d.get('as', '?')}",
        ]
        return "\n".join(lines)

    except httpx.HTTPError as e:
        return f"Geolocation request error: {e}"


def certificate_transparency(data: dict) -> str:
    """Search certificate transparency logs for a domain via crt.sh."""
    target = (data.get("domain") or data.get("target") or "").strip()
    if not target:
        return "domain is required."

    # Wildcard for all subdomains
    query = f"%.{target}"
    try:
        resp = httpx.get(
            "https://crt.sh/",
            params={"q": query, "output": "json"},
            timeout=REQUEST_TIMEOUT,
            headers={"Accept": "application/json"},
        )
        if resp.status_code != 200:
            return f"crt.sh request failed (HTTP {resp.status_code})"

        certs = resp.json()
        if not certs:
            return f"No certificates found in CT logs for {target}."

        # Deduplicate by name_value
        seen: set = set()
        unique: list[dict] = []
        for cert in certs:
            names = cert.get("name_value", "")
            for name in names.splitlines():
                name = name.strip().lower().lstrip("*.")
                if name and name not in seen:
                    seen.add(name)
                    unique.append(
                        {
                            "name": name,
                            "not_before": cert.get("not_before", "")[:10],
                            "issuer": cert.get("issuer_name", "")[:40],
                        }
                    )

        unique.sort(key=lambda x: x["name"])
        lines = [
            f"Certificate transparency results for {target}:",
            f"  Unique names found: {len(unique)}",
            "",
        ]
        for entry in unique[:50]:
            lines.append(f"  {entry['name']:<50}  issued={entry['not_before']}")
        if len(unique) > 50:
            lines.append(f"  ... and {len(unique) - 50} more")

        return "\n".join(lines)

    except httpx.HTTPError as e:
        return f"crt.sh request error: {e}"


def shodan_lookup(data: dict) -> str:
    """Search Shodan for internet-exposed services matching a query or IP."""
    api_key = os.environ.get("SHODAN_API_KEY", "")
    if not api_key:
        return (
            "Shodan not configured. Set SHODAN_API_KEY. "
            "Get a free key at https://account.shodan.io/"
        )

    query = (data.get("query") or data.get("target") or "").strip()
    if not query:
        return "query is required (e.g. 'hostname:example.com', 'net:203.0.113.0/24')."

    # Check if query looks like an IP for host lookup vs search
    is_ip = all(c.isdigit() or c == "." for c in query.split("/")[0])

    try:
        if is_ip:
            resp = httpx.get(
                f"https://api.shodan.io/shodan/host/{query}",
                params={"key": api_key},
                timeout=REQUEST_TIMEOUT,
            )
        else:
            resp = httpx.get(
                "https://api.shodan.io/shodan/host/search",
                params={"key": api_key, "query": query, "minify": True},
                timeout=REQUEST_TIMEOUT,
            )

        if resp.status_code == 401:
            return "Shodan API key is invalid or expired."
        if resp.status_code == 402:
            return "Shodan query requires a paid API plan."
        if resp.status_code != 200:
            return f"Shodan request failed (HTTP {resp.status_code})"

        result = resp.json()
        lines = [f"Shodan results for: {query}"]

        if is_ip:
            org = result.get("org", "?")
            country = result.get("country_name", "?")
            hostnames = result.get("hostnames", [])
            ports = result.get("ports", [])
            vulns = list(result.get("vulns", {}).keys())
            lines += [
                f"  IP: {query}",
                f"  Org: {org}  Country: {country}",
                f"  Hostnames: {', '.join(hostnames[:5]) or 'none'}",
                f"  Open ports: {', '.join(str(p) for p in sorted(ports)[:20])}",
            ]
            if vulns:
                lines.append(f"  ⚠ Vulnerabilities: {', '.join(vulns[:10])}")
            for service in result.get("data", [])[:10]:
                port = service.get("port", "?")
                transport = service.get("transport", "tcp")
                banner = (service.get("data") or "")[:80].replace("\n", " ")
                lines.append(f"  [{port}/{transport}] {banner}")
        else:
            total = result.get("total", 0)
            lines.append(f"  Total results: {total}")
            for match in result.get("matches", [])[:15]:
                ip = match.get("ip_str", "?")
                ports_str = str(match.get("port", "?"))
                org = (match.get("org") or "")[:30]
                country = match.get("location", {}).get("country_name", "?")[:20]
                lines.append(f"  {ip:<18} :{ports_str:<6} {country:<22} {org}")

        return "\n".join(lines)

    except httpx.HTTPError as e:
        return f"Shodan request error: {e}"


def search_breach_data(data: dict) -> str:
    """Check if an email address appears in known data breaches via Have I Been Pwned."""
    api_key = os.environ.get("HIBP_API_KEY", "")
    if not api_key:
        return (
            "Have I Been Pwned not configured. Set HIBP_API_KEY. "
            "Get a key at https://haveibeenpwned.com/API/Key"
        )

    email = (data.get("email") or data.get("target") or "").strip().lower()
    if not email or "@" not in email:
        return "A valid email address is required."

    try:
        resp = httpx.get(
            f"https://haveibeenpwned.com/api/v3/breachedaccount/{email}",
            headers={
                "hibp-api-key": api_key,
                "User-Agent": "Familiar-Agent/1.0",
            },
            params={"truncateResponse": "false"},
            timeout=REQUEST_TIMEOUT,
        )

        if resp.status_code == 404:
            return f"Good news: {email} was not found in any known data breaches."
        if resp.status_code == 401:
            return "HIBP API key is invalid."
        if resp.status_code == 429:
            return "HIBP rate limit reached. Wait a moment and try again."
        if resp.status_code != 200:
            return f"HIBP request failed (HTTP {resp.status_code})"

        breaches = resp.json()
        lines = [
            f"⚠ {email} found in {len(breaches)} breach(es):",
            "",
        ]
        for breach in sorted(breaches, key=lambda b: b.get("BreachDate", ""), reverse=True):
            name = breach.get("Name", "?")
            date = breach.get("BreachDate", "?")[:10]
            pwned = f"{breach.get('PwnCount', 0):,}"
            data_classes = ", ".join(breach.get("DataClasses", [])[:5])
            lines.append(f"  {date}  {name:<30}  {pwned:>12} accounts")
            lines.append(f"           Data: {data_classes}")

        return "\n".join(lines)

    except httpx.HTTPError as e:
        return f"HIBP request error: {e}"


# ---------------------------------------------------------------------------
# Tool definitions
# ---------------------------------------------------------------------------

TOOLS = [
    {
        "name": "confirm_osint_target",
        "description": (
            "Establish explicit authorization for OSINT reconnaissance against an external target. "
            "REQUIRED before using any other OSINT tool on an external entity. "
            "Constitution Article 8: the public record is not a license — "
            "provide a clear justification and scope for the research. "
            "Authorization is cached for 1 hour. "
            "Use this first, then proceed with reconnaissance tools."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "target": {
                    "type": "string",
                    "description": "The target entity (domain, IP, email, or organization name)",
                },
                "justification": {
                    "type": "string",
                    "description": (
                        "Legitimate purpose for the research. Be specific: "
                        "'Due diligence for business partnership', "
                        "'Authorized pentest — SOW ref #1234', "
                        "'Investigative journalism — public interest'"
                    ),
                },
                "scope": {
                    "type": "string",
                    "description": (
                        "What reconnaissance is authorized: "
                        "'Domain footprint, subdomains, DNS only', "
                        "'Full reconnaissance excluding personal contacts', "
                        "'Infrastructure mapping only'"
                    ),
                },
            },
            "required": ["target", "justification", "scope"],
        },
        "handler": confirm_osint_target,
        "category": "osint",
    },
    {
        "name": "harvest_emails",
        "description": (
            "Harvest email addresses associated with a domain using theHarvester. "
            "Searches multiple public sources (Google, Bing, Yahoo, DuckDuckGo). "
            "Also returns discovered hostnames. "
            "Requires confirm_osint_target() authorization first."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "domain": {"type": "string", "description": "Target domain (e.g. 'example.com')"},
                "sources": {
                    "type": "string",
                    "description": "Comma-separated sources",
                    "default": "google,bing,yahoo,duckduckgo",
                },
                "limit": {
                    "type": "integer",
                    "description": "Max results per source",
                    "default": 100,
                },
            },
            "required": ["domain"],
        },
        "handler": harvest_emails,
        "category": "osint",
    },
    {
        "name": "enumerate_subdomains",
        "description": (
            "Enumerate subdomains for a domain using Amass (passive OSINT) or DNS fallback. "
            "Passive mode uses only public data sources — no direct target contact. "
            "Active mode also performs DNS brute-forcing. "
            "Requires confirm_osint_target() authorization first."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "domain": {"type": "string", "description": "Target domain"},
                "passive": {
                    "type": "boolean",
                    "description": "Use passive mode only (no active probing)",
                    "default": True,
                },
            },
            "required": ["domain"],
        },
        "handler": enumerate_subdomains,
        "category": "osint",
    },
    {
        "name": "dns_enumerate",
        "description": (
            "Enumerate all DNS records for a domain: A, AAAA, MX, NS, TXT, SOA, CNAME. "
            "Also checks for SPF, DMARC, and common DKIM selectors. "
            "Uses dnspython if available, falls back to dig CLI. "
            "Requires confirm_osint_target() authorization first."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "domain": {"type": "string", "description": "Target domain to enumerate"},
            },
            "required": ["domain"],
        },
        "handler": dns_enumerate,
        "category": "osint",
    },
    {
        "name": "whois_lookup",
        "description": (
            "Perform a WHOIS lookup for a domain or IP address. "
            "Returns registrar, registration/expiry dates, nameservers, registrant org, and status. "
            "Uses python-whois library if available, falls back to whois CLI. "
            "Requires confirm_osint_target() authorization first."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "target": {"type": "string", "description": "Domain or IP address to look up"},
            },
            "required": ["target"],
        },
        "handler": whois_lookup,
        "category": "osint",
    },
    {
        "name": "ip_geolocation",
        "description": (
            "Geolocate an IP address using ip-api.com (free, no key required). "
            "Returns country, region, city, coordinates, ISP, organization, and ASN. "
            "Requires confirm_osint_target() authorization first for external IPs."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "ip": {"type": "string", "description": "IP address to geolocate"},
            },
            "required": ["ip"],
        },
        "handler": ip_geolocation,
        "category": "osint",
    },
    {
        "name": "certificate_transparency",
        "description": (
            "Search certificate transparency logs via crt.sh for all certificates "
            "issued to a domain and its subdomains. "
            "Reveals subdomains that CT logging exposes even without DNS records. "
            "Requires confirm_osint_target() authorization first."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "domain": {"type": "string", "description": "Target domain"},
            },
            "required": ["domain"],
        },
        "handler": certificate_transparency,
        "category": "osint",
    },
    {
        "name": "shodan_lookup",
        "description": (
            "Search Shodan for internet-exposed services. "
            "Accepts an IP for host lookup (open ports, banners, vulns) "
            "or a search query (e.g. 'hostname:example.com', 'org:\"Acme Corp\"'). "
            "Requires SHODAN_API_KEY environment variable. "
            "Requires confirm_osint_target() authorization first."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "query": {
                    "type": "string",
                    "description": "IP address or Shodan search query",
                },
            },
            "required": ["query"],
        },
        "handler": shodan_lookup,
        "category": "osint",
    },
    {
        "name": "search_breach_data",
        "description": (
            "Check if an email address appears in known data breaches via Have I Been Pwned. "
            "Returns breach names, dates, affected account counts, and data types exposed. "
            "Requires HIBP_API_KEY environment variable. "
            "Requires confirm_osint_target() authorization first."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "email": {"type": "string", "description": "Email address to check"},
            },
            "required": ["email"],
        },
        "handler": search_breach_data,
        "category": "osint",
    },
]

# ---------------------------------------------------------------------------
# Investigation persistence tools — import and register from investigations module
# ---------------------------------------------------------------------------

try:
    from familiar.skills.osint.investigations import (
        add_finding,
        create_investigation,
        get_investigation_findings,
        list_investigations,
        summarize_investigation,
        update_investigation_status,
    )

    TOOLS += [
        {
            "name": "create_investigation",
            "description": (
                "Create a new OSINT investigation case to track a target over time. "
                "Associates a name, target indicator, and description with a pipeline status "
                "(open → active → review → complete). "
                "Use this at the start of any sustained research effort."
            ),
            "input_schema": {
                "type": "object",
                "properties": {
                    "name": {
                        "type": "string",
                        "description": "Short label for the investigation (e.g. 'Example Corp Footprint')",
                    },
                    "target": {
                        "type": "string",
                        "description": "Primary indicator: domain, IP, email, person name, etc.",
                    },
                    "description": {
                        "type": "string",
                        "description": "Justification and scope for the investigation",
                    },
                },
                "required": ["name", "target"],
            },
            "handler": create_investigation,
            "category": "osint",
        },
        {
            "name": "update_investigation_status",
            "description": (
                "Move an investigation through the pipeline: "
                "open → active → review → complete (or archived). "
                "Update status when the investigation transitions stages."
            ),
            "input_schema": {
                "type": "object",
                "properties": {
                    "investigation_id": {
                        "type": "integer",
                        "description": "ID returned by create_investigation",
                    },
                    "status": {
                        "type": "string",
                        "enum": ["open", "active", "review", "complete", "archived"],
                        "description": "New status for the investigation",
                    },
                },
                "required": ["investigation_id", "status"],
            },
            "handler": update_investigation_status,
            "category": "osint",
        },
        {
            "name": "add_finding",
            "description": (
                "Record a discrete finding (indicator of compromise, entity, or data point) "
                "discovered during an investigation. Supports types: ip, domain, email, "
                "subdomain, credential, person, org, phone, certificate, social_profile, "
                "vulnerability, netblock, other. "
                "Findings appear in the OSINT workspace entity table and are cross-referenced "
                "across investigations to surface repeat indicators."
            ),
            "input_schema": {
                "type": "object",
                "properties": {
                    "investigation_id": {
                        "type": "integer",
                        "description": "ID of the parent investigation",
                    },
                    "finding_type": {
                        "type": "string",
                        "description": (
                            "Type: ip, domain, email, subdomain, credential, person, "
                            "org, phone, certificate, social_profile, vulnerability, netblock, other"
                        ),
                    },
                    "value": {
                        "type": "string",
                        "description": "The actual indicator value (e.g. '192.0.2.1', 'user@example.com')",
                    },
                    "source_tool": {
                        "type": "string",
                        "description": "Tool that discovered this (e.g. 'spiderfoot', 'shodan', 'manual')",
                    },
                    "risk_level": {
                        "type": "string",
                        "enum": ["critical", "high", "medium", "low", "info"],
                        "description": "Severity assessment. Default: info",
                    },
                    "notes": {
                        "type": "string",
                        "description": "Optional analyst notes about the significance of this finding",
                    },
                },
                "required": ["investigation_id", "finding_type", "value", "source_tool"],
            },
            "handler": add_finding,
            "category": "osint",
        },
        {
            "name": "list_investigations",
            "description": (
                "List OSINT investigations, optionally filtered by status. "
                "Returns pipeline-ready rows with finding counts. "
                "Use status_filter='active' to see current open cases."
            ),
            "input_schema": {
                "type": "object",
                "properties": {
                    "status_filter": {
                        "type": "string",
                        "enum": ["open", "active", "review", "complete", "archived"],
                        "description": "Filter by status (omit to list all investigations)",
                    },
                },
                "required": [],
            },
            "handler": list_investigations,
            "category": "osint",
        },
        {
            "name": "get_investigation_findings",
            "description": (
                "Get all findings for a specific investigation, grouped by type and risk level. "
                "Use type_filter to narrow to a specific indicator class (e.g. 'email', 'ip')."
            ),
            "input_schema": {
                "type": "object",
                "properties": {
                    "investigation_id": {
                        "type": "integer",
                        "description": "ID of the investigation",
                    },
                    "type_filter": {
                        "type": "string",
                        "description": "Optional: filter by finding type (ip, domain, email, etc.)",
                    },
                },
                "required": ["investigation_id"],
            },
            "handler": get_investigation_findings,
            "category": "osint",
        },
        {
            "name": "summarize_investigation",
            "description": (
                "Generate a summary of an investigation: finding counts by type and risk, "
                "and a list of notable (high/critical) findings. "
                "Use before closing an investigation or writing a final report."
            ),
            "input_schema": {
                "type": "object",
                "properties": {
                    "investigation_id": {
                        "type": "integer",
                        "description": "ID of the investigation to summarize",
                    },
                },
                "required": ["investigation_id"],
            },
            "handler": summarize_investigation,
            "category": "osint",
        },
    ]
except ImportError as _inv_import_err:
    logger.debug("OSINT investigations module unavailable: %s", _inv_import_err)
