#!/usr/bin/env python3
"""Bomber Company agent-context CLI. Python 3.9+, standard library only."""

import argparse
import hashlib
import json
import os
import pathlib
import re
import sys
import urllib.error
import urllib.parse
import urllib.request


CLI_VERSION = "0.1.0"
SCHEMA = "boooooom.company-agent/v1"
DEFAULT_API = os.environ.get("BOMBER_COMPANY_API", "https://boooooom.shop").rstrip("/")
MANIFEST_PATH = "/api/v1/company/agent-manifest"
WHITEPAPER_ID = "contribution-points-whitepaper"
HEADING_RE = re.compile(r"^(#{1,6})\s+(.+?)\s*$")
SECTION_NUMBER_RE = re.compile(r"^(?:\d+(?:\.\d+)*\.?|[A-Z]\.)\s+", re.IGNORECASE)


class CliError(RuntimeError):
    pass


def normalize_api(value):
    api = str(value or DEFAULT_API).rstrip("/")
    parsed = urllib.parse.urlsplit(api)
    if parsed.scheme not in {"http", "https"} or not parsed.netloc or parsed.query or parsed.fragment:
        raise CliError("API base URL must be an http(s) origin or path prefix")
    return api


def absolute_url(api, value):
    return urllib.parse.urljoin(normalize_api(api) + "/", str(value or "").lstrip("/"))


def request_bytes(api, path, timeout=30, accept="application/json"):
    url = absolute_url(api, path)
    request = urllib.request.Request(
        url,
        headers={"Accept": accept, "User-Agent": f"bomber-company-cli/{CLI_VERSION}"},
    )
    try:
        with urllib.request.urlopen(request, timeout=timeout) as response:
            return response.read(), response.headers.get("Content-Type", "")
    except urllib.error.HTTPError as exc:
        try:
            payload = json.loads(exc.read().decode("utf-8"))
            message = payload.get("error") or f"HTTP {exc.code}"
        except Exception:
            message = f"HTTP {exc.code}"
        raise CliError(f"Request failed for {url}: {message}") from exc
    except (urllib.error.URLError, TimeoutError) as exc:
        raise CliError(f"Cannot reach {url}: {exc}") from exc


def request_json(api, path, timeout=30):
    content, _ = request_bytes(api, path, timeout=timeout)
    try:
        payload = json.loads(content.decode("utf-8"))
    except (UnicodeDecodeError, json.JSONDecodeError) as exc:
        raise CliError(f"Endpoint did not return valid UTF-8 JSON: {path}") from exc
    if not isinstance(payload, dict):
        raise CliError(f"Endpoint returned an unexpected JSON value: {path}")
    return payload


def get_manifest(args):
    payload = request_json(args.api, MANIFEST_PATH, timeout=args.timeout)
    if payload.get("schema") != SCHEMA or not isinstance(payload.get("resources"), list):
        raise CliError(f"Unsupported agent manifest schema: {payload.get('schema')!r}")
    return payload


def whitepaper_resource(manifest):
    for resource in manifest.get("resources", []):
        if isinstance(resource, dict) and resource.get("id") == WHITEPAPER_ID:
            return resource
    raise CliError(f"Manifest does not advertise resource: {WHITEPAPER_ID}")


def verified_whitepaper(args, manifest=None):
    manifest = manifest or get_manifest(args)
    resource = whitepaper_resource(manifest)
    content, _ = request_bytes(
        args.api,
        resource.get("contentUrl"),
        timeout=args.timeout,
        accept="text/markdown",
    )
    expected_size = resource.get("bytes")
    expected_sha = str(resource.get("sha256") or "").lower()
    actual_sha = hashlib.sha256(content).hexdigest()
    if isinstance(expected_size, int) and len(content) != expected_size:
        raise CliError(f"White paper size mismatch: expected {expected_size}, got {len(content)}")
    if not re.fullmatch(r"[a-f0-9]{64}", expected_sha) or actual_sha != expected_sha:
        raise CliError(f"White paper SHA-256 mismatch: expected {expected_sha}, got {actual_sha}")
    try:
        text = content.decode("utf-8")
    except UnicodeDecodeError as exc:
        raise CliError("White paper is not valid UTF-8") from exc
    return resource, content, text


def markdown_sections(text):
    lines = text.splitlines()
    headings = []
    stack = []
    for line_number, line in enumerate(lines, start=1):
        match = HEADING_RE.match(line)
        if not match:
            continue
        level = len(match.group(1))
        title = match.group(2).strip().rstrip("#").strip()
        while stack and stack[-1][0] >= level:
            stack.pop()
        path = [item[1] for item in stack] + [title]
        headings.append({
            "id": f"section-{len(headings) + 1:03d}",
            "level": level,
            "title": title,
            "path": path,
            "lineStart": line_number,
            "lineEnd": len(lines),
        })
        stack.append((level, title))
    for index, section in enumerate(headings):
        for candidate in headings[index + 1:]:
            if candidate["level"] <= section["level"]:
                section["lineEnd"] = candidate["lineStart"] - 1
                break
    return headings


def normalized_section_title(value):
    title = str(value or "").strip()
    return SECTION_NUMBER_RE.sub("", title).casefold()


def choose_section(sections, query):
    raw_key = str(query or "").strip().casefold()
    normalized_key = normalized_section_title(query)
    if not raw_key:
        raise CliError("Section query cannot be empty")
    exact = [item for item in sections if item["title"].casefold() == raw_key
             or normalized_section_title(item["title"]) == normalized_key
             or " / ".join(item["path"]).casefold() == raw_key]
    title_matches = [item for item in sections if raw_key in item["title"].casefold()
                     or normalized_key in normalized_section_title(item["title"])]
    path_matches = [item for item in sections if raw_key in " / ".join(item["path"]).casefold()]
    matches = exact or title_matches or path_matches
    if not matches:
        raise CliError(f"No section matches: {query}")
    if len(matches) > 1:
        options = "; ".join(f"{item['id']} {item['title']}" for item in matches[:8])
        raise CliError(f"Section query is ambiguous: {options}")
    return matches[0]


def section_content(text, section):
    lines = text.splitlines()
    return "\n".join(lines[section["lineStart"] - 1:section["lineEnd"]]).rstrip() + "\n"


def search_markdown(text, query, context=1, limit=20):
    key = str(query or "").strip().casefold()
    if not key:
        raise CliError("Search query cannot be empty")
    lines = text.splitlines()
    results = []
    for index, line in enumerate(lines):
        if key not in line.casefold():
            continue
        start = max(0, index - context)
        end = min(len(lines), index + context + 1)
        results.append({
            "line": index + 1,
            "text": line,
            "context": [{"line": number + 1, "text": lines[number]} for number in range(start, end)],
        })
        if len(results) >= limit:
            break
    return results


def print_json(value):
    print(json.dumps(value, ensure_ascii=False, indent=2))


def command_manifest(args):
    manifest = get_manifest(args)
    if args.json:
        print_json(manifest)
        return
    print(f"{manifest.get('name')} ({manifest.get('schema')})")
    print(f"Phase: {manifest.get('phase')}  Version: {manifest.get('version')}")
    for name, capability in manifest.get("capabilities", {}).items():
        print(f"{name}\t{capability.get('status', 'unknown')}")
    for resource in manifest.get("resources", []):
        print(f"{resource.get('id')}\t{resource.get('locale')}\t{resource.get('bytes')} bytes")


def command_whitepaper_read(args):
    resource, _, text = verified_whitepaper(args)
    section = None
    if args.section:
        section = choose_section(markdown_sections(text), args.section)
        text = section_content(text, section)
    if args.json:
        print_json({"resource": resource, "section": section, "content": text})
    else:
        sys.stdout.write(text)


def command_whitepaper_sections(args):
    resource, _, text = verified_whitepaper(args)
    sections = markdown_sections(text)
    if args.json:
        print_json({"resourceId": resource["id"], "sections": sections})
        return
    for section in sections:
        indent = "  " * max(0, section["level"] - 1)
        print(f"{section['id']}\t{section['lineStart']}-{section['lineEnd']}\t{indent}{section['title']}")


def command_whitepaper_search(args):
    resource, _, text = verified_whitepaper(args)
    results = search_markdown(text, args.query, context=args.context, limit=args.limit)
    if args.json:
        print_json({"resourceId": resource["id"], "query": args.query, "matches": results})
        return
    if not results:
        print("No matches.")
        return
    for result in results:
        print(f"{result['line']}: {result['text']}")


def command_whitepaper_download(args):
    resource, content, _ = verified_whitepaper(args)
    output = pathlib.Path(args.output or resource.get("filename") or "company-whitepaper.md").resolve()
    if output.exists() and not args.force:
        raise CliError(f"Output already exists; use --force to replace it: {output}")
    output.parent.mkdir(parents=True, exist_ok=True)
    temporary = output.with_name(output.name + ".part")
    try:
        temporary.write_bytes(content)
        os.replace(temporary, output)
    finally:
        if temporary.exists():
            temporary.unlink()
    print(f"Downloaded: {output}")
    print(f"Verified SHA-256: {resource['sha256']}")


def parser():
    root = argparse.ArgumentParser(
        prog="bomber-company",
        description="Read verified Bomber Company context for agent integration",
    )
    root.add_argument("--version", action="version", version=f"bomber-company {CLI_VERSION}")
    root.add_argument("--api", default=DEFAULT_API, help=f"API base URL (default: {DEFAULT_API})")
    root.add_argument("--timeout", type=int, default=30, help="HTTP timeout in seconds (default: 30)")
    commands = root.add_subparsers(dest="command", required=True)

    manifest = commands.add_parser("manifest", help="Discover available and planned capabilities")
    manifest.add_argument("--json", action="store_true")
    manifest.set_defaults(func=command_manifest)

    whitepaper = commands.add_parser("whitepaper", help="Read the contribution-points white paper")
    whitepaper_commands = whitepaper.add_subparsers(dest="whitepaper_command", required=True)

    read = whitepaper_commands.add_parser("read", help="Write verified Markdown to stdout")
    read.add_argument("--section", help="Exact or unique partial section title/path")
    read.add_argument("--json", action="store_true", help="Return a JSON envelope")
    read.set_defaults(func=command_whitepaper_read)

    sections = whitepaper_commands.add_parser("sections", help="List the Markdown section index")
    sections.add_argument("--json", action="store_true")
    sections.set_defaults(func=command_whitepaper_sections)

    search = whitepaper_commands.add_parser("search", help="Search the verified Markdown")
    search.add_argument("query")
    search.add_argument("--context", type=int, choices=range(0, 6), default=1)
    search.add_argument("--limit", type=int, choices=range(1, 101), default=20)
    search.add_argument("--json", action="store_true")
    search.set_defaults(func=command_whitepaper_search)

    download = whitepaper_commands.add_parser("download", help="Download and verify the Markdown")
    download.add_argument("--output", "-o")
    download.add_argument("--force", action="store_true")
    download.set_defaults(func=command_whitepaper_download)
    return root


def main():
    if hasattr(sys.stdout, "reconfigure"):
        sys.stdout.reconfigure(encoding="utf-8", errors="replace")
        sys.stderr.reconfigure(encoding="utf-8", errors="replace")
    args = parser().parse_args()
    try:
        args.api = normalize_api(args.api)
        if not 1 <= args.timeout <= 300:
            raise CliError("--timeout must be between 1 and 300 seconds")
        args.func(args)
    except (CliError, OSError, json.JSONDecodeError) as exc:
        print(f"error: {exc}", file=sys.stderr)
        return 1
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
