from __future__ import annotations

import argparse
import json
from typing import Any, Dict, List, Optional, Tuple


_CLI_MANIFEST_SCHEMA_VERSION = 2


_AUTH_MODE_EXACT: Dict[str, str] = {
    "setup": "none",
    "auth.bootstrap": "none",
    "auth.login": "none",
    "auth.set-key": "none",
    "auth.show": "none",
    "auth.clear": "none",
    "onboarding.submit-domain": "user_session",
    "org-context.get": "user_session",
    "org-context.set": "user_session",
    "personal-context.get": "user_session",
    "personal-context.set": "user_session",
    "context.resolved": "user_session",
    "capabilities": "optional",
    "webhooks.ingest": "webhook_token",
    "cli-manifest": "none",
    "cli-reference": "none",
    "schema": "none",
    "status": "none",
    "watch": "none",
    "sop": "none",
}

_AUTH_MODE_PREFIXES: Tuple[Tuple[str, str], ...] = (
    ("auth.", "developer_key_or_user_session"),
    ("onboarding.", "user_session"),
    ("org-context.", "user_session"),
    ("personal-context.", "user_session"),
    ("context.", "user_session"),
    ("workspace-secrets.", "developer_key_or_user_session"),
    ("tables.", "developer_key_or_user_session"),
    ("blacklist.", "developer_key_or_user_session"),
    ("rows.", "developer_key_or_user_session"),
    ("cells.", "developer_key_or_user_session"),
    ("columns.", "developer_key_or_user_session"),
    ("sequences.", "developer_key_or_user_session"),
    ("leads.", "developer_key_or_user_session"),
    ("tasks.", "developer_key_or_user_session"),
    ("jobs.", "developer_key_or_user_session"),
    ("webhooks.", "developer_key_or_user_session"),
    ("search.", "developer_key_or_user_session"),
    ("linkedin.", "developer_key_or_user_session"),
    ("workflows.", "none"),
)

_STREAMING_COMMAND_KEYS = {
    "columns.run",
    "columns.run-next",
    "jobs.watch",
    "rows.import-csv",
    "rows.import-status",
    "sequences.enroll",
}

_OPTIONAL_DEPENDENCY_COMMANDS: Dict[str, Dict[str, Any]] = {
    "status": {
        "kind": "optional_dependency",
        "python_modules": ["pymongo"],
        "env": ["MONGODB_URI"],
        "notes": [
            "Reads status directly from MongoDB instead of the HTTP API.",
            "Requires pymongo at runtime and MONGODB_URI (or --mongo-uri).",
        ],
    },
    "watch": {
        "kind": "optional_dependency",
        "python_modules": ["pymongo"],
        "env": ["MONGODB_URI"],
        "notes": [
            "Streams status directly from MongoDB instead of the HTTP API.",
            "Requires pymongo at runtime and MONGODB_URI (or --mongo-uri).",
        ],
    },
}

_SENSITIVE_OPTION_TERMS = ("token", "api-key", "api_key", "password", "secret")


def _json_safe_value(value: Any) -> Any:
    try:
        json.dumps(value)
    except (TypeError, ValueError):
        return str(value)
    return value


def _option_input_kind(action: argparse.Action) -> Optional[str]:
    flags = [flag.lower() for flag in getattr(action, "option_strings", []) or []]
    dest = str(getattr(action, "dest", "") or "").lower()
    if any(flag.endswith("-file") or flag == "--file" for flag in flags) or dest.endswith("_file"):
        return "file"
    if any(flag.endswith("-json") for flag in flags) or dest.endswith("_json"):
        return "json"
    if dest in {"out_file", "checkpoint_file"}:
        return "file"
    return None


def _option_kind(action: argparse.Action, *, input_kind: Optional[str]) -> str:
    if isinstance(action, (argparse._StoreTrueAction, argparse._StoreFalseAction)):
        return "boolean"
    if isinstance(action, argparse._CountAction):
        return "integer"
    if input_kind:
        return input_kind
    if getattr(action, "type", None) is int:
        return "integer"
    if getattr(action, "type", None) is float:
        return "number"
    if getattr(action, "nargs", None) in ("*", "+"):
        return "list"
    return "string"


def _option_action_kind(action: argparse.Action) -> str:
    if isinstance(action, argparse._StoreTrueAction):
        return "store_true"
    if isinstance(action, argparse._StoreFalseAction):
        return "store_false"
    if isinstance(action, argparse._AppendAction):
        return "append"
    if isinstance(action, argparse._AppendConstAction):
        return "append_const"
    if isinstance(action, argparse._CountAction):
        return "count"
    if isinstance(action, argparse._StoreConstAction):
        return "store_const"
    return "store"


def _option_is_sensitive(action: argparse.Action) -> bool:
    option_strings = [flag.lower() for flag in getattr(action, "option_strings", []) or []]
    dest = str(getattr(action, "dest", "") or "").lower()
    if isinstance(action, (argparse._StoreTrueAction, argparse._StoreFalseAction, argparse._StoreConstAction, argparse._AppendConstAction)):
        return False
    if dest == "value" or "--value" in option_strings:
        return True
    searchable = " ".join([dest, *option_strings])
    return any(term in searchable for term in _SENSITIVE_OPTION_TERMS)


def _option_accepts_comma_separated(action: argparse.Action) -> bool:
    help_text = str(getattr(action, "help", "") or "").lower()
    return "comma-separated" in help_text


def _option_repeatable(action: argparse.Action) -> bool:
    return isinstance(action, (argparse._AppendAction, argparse._AppendConstAction, argparse._CountAction))


def canonical_subparser_entries(
    action: argparse._SubParsersAction,
) -> List[Tuple[str, argparse.ArgumentParser, List[str], Optional[str]]]:
    names_by_parser: Dict[int, List[str]] = {}
    parser_by_id: Dict[int, argparse.ArgumentParser] = {}
    for name, parser in action._name_parser_map.items():
        parser_id = id(parser)
        parser_by_id[parser_id] = parser
        names_by_parser.setdefault(parser_id, []).append(name)

    help_by_name = {choice.dest: choice.help for choice in action._choices_actions}
    entries: List[Tuple[str, argparse.ArgumentParser, List[str], Optional[str]]] = []
    seen: set[int] = set()

    for choice in action._choices_actions:
        parser = action._name_parser_map.get(choice.dest)
        if parser is None:
            continue
        parser_id = id(parser)
        if parser_id in seen:
            continue
        seen.add(parser_id)
        aliases = sorted(name for name in names_by_parser.get(parser_id, []) if name != choice.dest)
        entries.append((choice.dest, parser, aliases, choice.help))

    for parser_id, parser in parser_by_id.items():
        if parser_id in seen:
            continue
        names = sorted(names_by_parser.get(parser_id, []))
        if not names:
            continue
        canonical = names[0]
        entries.append((canonical, parser, [name for name in names if name != canonical], help_by_name.get(canonical)))

    return entries


def action_descriptor(action: argparse.Action) -> Optional[Dict[str, Any]]:
    if isinstance(action, argparse._HelpAction):
        return None
    if isinstance(action, argparse._SubParsersAction):
        return None

    input_kind = _option_input_kind(action)
    action_kind = _option_action_kind(action)
    payload: Dict[str, Any] = {
        "dest": action.dest,
        "required": bool(getattr(action, "required", False)),
        "help": action.help,
        "kind": _option_kind(action, input_kind=input_kind),
        "action": action_kind,
    }
    if getattr(action, "choices", None) is not None:
        payload["choices"] = list(action.choices)
    if getattr(action, "nargs", None) is not None:
        payload["nargs"] = action.nargs
    if getattr(action, "metavar", None) is not None:
        payload["metavar"] = action.metavar
    default_value = getattr(action, "default", argparse.SUPPRESS)
    if default_value is not argparse.SUPPRESS:
        if default_value is not None or isinstance(action, (argparse._StoreTrueAction, argparse._StoreFalseAction)):
            payload["default_when_omitted"] = _json_safe_value(default_value)
        if default_value is not None and not isinstance(action, (argparse._StoreTrueAction, argparse._StoreFalseAction)):
            payload["default"] = _json_safe_value(default_value)
    if isinstance(action, argparse._StoreTrueAction):
        payload["value_when_present"] = True
    elif isinstance(action, argparse._StoreFalseAction):
        payload["value_when_present"] = False
    if input_kind:
        payload["input_kind"] = input_kind
    if _option_repeatable(action):
        payload["repeatable"] = True
    if _option_accepts_comma_separated(action):
        payload["accepts_comma_separated"] = True
    if _option_is_sensitive(action):
        payload["sensitive"] = True
    if action.option_strings:
        payload["flags"] = list(action.option_strings)
        payload["takes_value"] = not isinstance(
            action,
            (argparse._StoreTrueAction, argparse._StoreFalseAction, argparse._StoreConstAction, argparse._AppendConstAction),
        ) and getattr(action, "nargs", None) != 0
    else:
        payload["name"] = action.dest
        payload["positional"] = True
    return payload


def infer_auth_mode(command_key: str, parser: argparse.ArgumentParser, *, has_subcommands: bool) -> str:
    exact = _AUTH_MODE_EXACT.get(command_key)
    if exact:
        return exact
    if command_key.endswith(".recipe") or command_key.endswith(".schema"):
        return "none"
    if has_subcommands:
        return "varies_by_subcommand"
    for prefix, auth_mode in _AUTH_MODE_PREFIXES:
        if command_key.startswith(prefix):
            return auth_mode
    return "developer_key_or_user_session"


def infer_destructive(command_key: str, parser: argparse.ArgumentParser) -> bool:
    for action in parser._actions:
        if "--yes" in getattr(action, "option_strings", []):
            return True
    return any(token in {"delete", "clear"} for token in command_key.split("."))


def infer_availability(command_key: str) -> Optional[Dict[str, Any]]:
    payload = _OPTIONAL_DEPENDENCY_COMMANDS.get(command_key)
    if not payload:
        return None
    return dict(payload)


def output_contract(command_key: str, parser: argparse.ArgumentParser) -> Optional[Dict[str, Any]]:
    output_action = next((action for action in parser._actions if getattr(action, "dest", None) == "output"), None)
    if output_action is None:
        availability = infer_availability(command_key)
        if availability and command_key == "watch":
            return {
                "modes": ["human"],
                "human": {"interactive": True, "streaming": True},
            }
        return None
    modes = list(getattr(output_action, "choices", None) or [])
    payload: Dict[str, Any] = {"modes": modes}
    if "json" in modes:
        payload["json"] = {"single_document": True}
    if "ndjson" in modes:
        payload["ndjson"] = {
            "streaming": True,
            "recommended_for": "progress updates / watch flows",
        }
    if "human" in modes:
        payload["human"] = {"interactive": True}
    if command_key in _STREAMING_COMMAND_KEYS:
        payload["streaming_command"] = True
    return payload


def command_examples(parser: argparse.ArgumentParser) -> List[str]:
    extra_examples = getattr(parser, "_codex_examples", None)
    if isinstance(extra_examples, list) and extra_examples:
        return [str(example) for example in extra_examples if str(example).strip()]
    usage = parser.format_usage().strip()
    if usage.startswith("usage: "):
        usage = usage[len("usage: ") :]
    return [usage]


def mutually_exclusive_groups(parser: argparse.ArgumentParser) -> List[Dict[str, Any]]:
    groups: List[Dict[str, Any]] = []
    for group in getattr(parser, "_mutually_exclusive_groups", []) or []:
        option_flags: List[str] = []
        for action in getattr(group, "_group_actions", []) or []:
            flags = list(getattr(action, "option_strings", []) or [])
            if flags:
                option_flags.append(flags[0])
        if option_flags:
            groups.append(
                {
                    "required": bool(getattr(group, "required", False)),
                    "options": option_flags,
                }
            )
    return groups


def collect_cli_command_manifest(
    parser: argparse.ArgumentParser,
    *,
    path_parts: Optional[List[str]] = None,
) -> Dict[str, Dict[str, Any]]:
    path_parts = path_parts or ["autotouch"]
    command_key = ".".join(path_parts[1:])
    subparser_action = next((action for action in parser._actions if isinstance(action, argparse._SubParsersAction)), None)
    has_subcommands = subparser_action is not None

    options: List[Dict[str, Any]] = []
    positionals: List[Dict[str, Any]] = []
    for action in parser._actions:
        descriptor = action_descriptor(action)
        if not descriptor:
            continue
        if descriptor.get("positional"):
            positionals.append(descriptor)
        else:
            options.append(descriptor)

    commands: Dict[str, Dict[str, Any]] = {}
    if command_key:
        required_flags = [
            descriptor["flags"][0]
            for descriptor in options
            if descriptor.get("required") and descriptor.get("flags")
        ]
        subcommands: List[str] = []
        if has_subcommands and subparser_action is not None:
            subcommands = [entry[0] for entry in canonical_subparser_entries(subparser_action)]
        availability = infer_availability(command_key)
        commands[command_key] = {
            "canonical": list(path_parts),
            "aliases": [],
            "group": path_parts[1] if len(path_parts) > 1 else None,
            "help": parser.description or parser.format_usage().strip(),
            "description": parser.description,
            "notes": parser.epilog,
            "required_flags": required_flags,
            "positionals": positionals,
            "options": options,
            "mutually_exclusive_groups": mutually_exclusive_groups(parser),
            "subcommands": subcommands,
            "leaf_command": not has_subcommands,
            "auth_mode": infer_auth_mode(command_key, parser, has_subcommands=has_subcommands),
            "destructive": infer_destructive(command_key, parser),
            "output_contract": output_contract(command_key, parser),
            "availability": availability,
            "examples": command_examples(parser),
            "stability": "stable",
            "handler": getattr(parser.get_default("func"), "__name__", None),
        }

    if subparser_action is None:
        return commands

    for name, child_parser, aliases, help_text in canonical_subparser_entries(subparser_action):
        child_path = [*path_parts, name]
        child_key = ".".join(child_path[1:])
        child_commands = collect_cli_command_manifest(child_parser, path_parts=child_path)
        if child_key in child_commands:
            child_commands[child_key]["aliases"] = aliases
            if help_text:
                child_commands[child_key]["help"] = help_text
        commands.update(child_commands)

    return commands


def build_cli_manifest(version: str, parser: argparse.ArgumentParser) -> Dict[str, Any]:
    return {
        "version": version,
        "manifest_schema_version": _CLI_MANIFEST_SCHEMA_VERSION,
        "entry_points": {
            "autotouch": "autotouch_cli.cli:main",
            "smart-table": "autotouch_cli.cli:main",
        },
        "commands": collect_cli_command_manifest(parser),
    }


def build_cli_reference_markdown(manifest: Dict[str, Any]) -> str:
    version = str(manifest.get("version") or "unknown")
    manifest_schema_version = str(manifest.get("manifest_schema_version") or "unknown")
    commands = manifest.get("commands") if isinstance(manifest.get("commands"), dict) else {}
    lines: List[str] = [
        "# Autotouch CLI Reference",
        "",
        f"Generated from the installed parser for `autotouch-cli` `{version}`.",
        f"Manifest schema version: `{manifest_schema_version}`.",
        "",
        "## Output Modes",
        "",
        "- `json`: exactly one JSON document per invocation.",
        "- `ndjson`: one JSON object per line for streaming/progress-oriented commands.",
        "- `human`: interactive text for people.",
        "",
        "## Commands",
        "",
    ]

    top_level_groups = sorted({str(key).split(".")[0] for key in commands.keys() if key})
    for group in top_level_groups:
        lines.append(f"### `{group}`")
        lines.append("")
        group_items = [
            (key, commands[key])
            for key in sorted(commands.keys())
            if key == group or key.startswith(f"{group}.")
        ]
        for key, entry in group_items:
            canonical = " ".join(entry.get("canonical") or [])
            lines.append(f"#### `{canonical}`")
            lines.append("")
            help_text = str(entry.get("help") or "").strip()
            if help_text:
                lines.append(help_text)
                lines.append("")
            description = str(entry.get("description") or "").strip()
            if description and description != help_text:
                lines.append(description)
                lines.append("")
            lines.append(f"- Auth: `{entry.get('auth_mode') or 'unknown'}`")
            lines.append(f"- Stability: `{entry.get('stability') or 'stable'}`")
            lines.append(f"- Destructive: `{'yes' if entry.get('destructive') else 'no'}`")
            availability = entry.get("availability") or {}
            if availability:
                modules = ", ".join(availability.get("python_modules") or [])
                env_vars = ", ".join(availability.get("env") or [])
                lines.append(f"- Availability: `{availability.get('kind') or 'optional'}`")
                if modules:
                    lines.append(f"- Optional Python modules: `{modules}`")
                if env_vars:
                    lines.append(f"- Required env/config: `{env_vars}`")
            aliases = entry.get("aliases") or []
            if aliases:
                lines.append(f"- Aliases: `{', '.join(aliases)}`")
            required_flags = entry.get("required_flags") or []
            if required_flags:
                lines.append(f"- Required flags: `{', '.join(required_flags)}`")
            output = entry.get("output_contract") or {}
            modes = output.get("modes") or []
            if modes:
                lines.append(f"- Output modes: `{', '.join(modes)}`")
            subcommands = entry.get("subcommands") or []
            if subcommands:
                lines.append(f"- Subcommands: `{', '.join(subcommands)}`")
            examples = entry.get("examples") or []
            if examples:
                lines.append("- Example:")
                for example in examples:
                    lines.append(f"  - `{example}`")
            notes = str(entry.get("notes") or "").strip()
            if notes:
                lines.append("- Notes:")
                lines.append("")
                lines.append("```text")
                lines.extend(notes.rstrip().splitlines())
                lines.append("```")
            options = entry.get("options") or []
            if options:
                lines.append("- Options:")
                for option in options:
                    flags = option.get("flags") or []
                    flag_label = ", ".join(flags) if flags else option.get("name") or option.get("dest") or "<arg>"
                    note_parts: List[str] = []
                    if option.get("required"):
                        note_parts.append("required")
                    if option.get("kind"):
                        note_parts.append(f"kind={option.get('kind')}")
                    if option.get("input_kind"):
                        note_parts.append(f"input={option.get('input_kind')}")
                    if option.get("repeatable"):
                        note_parts.append("repeatable")
                    if option.get("accepts_comma_separated"):
                        note_parts.append("comma-separated")
                    if option.get("sensitive"):
                        note_parts.append("sensitive")
                    if option.get("action") in {"store_true", "store_false"}:
                        note_parts.append(
                            f"when omitted={option.get('default_when_omitted')}; when present={option.get('value_when_present')}"
                        )
                    if option.get("choices"):
                        note_parts.append(f"choices={','.join(str(v) for v in option.get('choices') or [])}")
                    if option.get("default") is not None and option.get("action") not in {"store_true", "store_false"}:
                        note_parts.append(f"default={option.get('default')}")
                    help_value = str(option.get("help") or "").strip()
                    note_text = f" ({'; '.join(note_parts)})" if note_parts else ""
                    if help_value:
                        lines.append(f"  - `{flag_label}`{note_text}: {help_value}")
                    else:
                        lines.append(f"  - `{flag_label}`{note_text}")
            lines.append("")
    return "\n".join(lines).rstrip() + "\n"
