from __future__ import annotations

from flow_xhs.cli.spec.enums import ConstraintSeverity, ParameterGroup, RequirednessMode
from flow_xhs.cli.spec.model import CliSpec, CommandSpec, ConstraintSpec, DocTopicSpec, ParameterSpec
from flow_xhs.cli.spec.views import GroupView, list_root_entries


GROUP_TITLES: dict[ParameterGroup, str] = {
    ParameterGroup.REQUIRED_INPUTS: "Required Inputs",
    ParameterGroup.CONDITIONAL_INPUTS: "Conditional Inputs",
    ParameterGroup.OPTIONAL_INPUTS: "Optional Inputs",
    ParameterGroup.INPUT_SOURCES: "Input Sources",
    ParameterGroup.EXECUTION_CONTROLS: "Execution Controls",
    ParameterGroup.OUTPUT_CONTROLS: "Output Controls",
    ParameterGroup.ADVANCED: "Advanced",
}


def render_root_help(spec: CliSpec) -> str:
    lines: list[str] = [spec.summary]
    if spec.description:
        lines.extend(["", spec.description])

    lines.extend(
        [
            "",
            "Usage:",
            "  flow-xhs --version",
            "  flow-xhs [COMMAND] --help",
            f"  {spec.canonical_syntax}",
        ]
    )

    lines.extend(["", "Commands:"])
    for entry in list_root_entries(spec):
        lines.append(f"  {entry.name:<20} {entry.summary}")

    visible_global_options = _visible_global_options(spec)
    if visible_global_options:
        lines.extend(["", "Options:"])
        for param in visible_global_options:
            lines.extend(_render_parameter_block(param, indent="  "))

    quick_start = tuple(spec.metadata.get("quick_start", ()))
    if quick_start:
        lines.extend(["", "Use Case:"])
        for example in quick_start:
            lines.append(f"  {example}")

    if spec.topics:
        lines.extend(["", "Reference Notes:"])
        for topic in spec.topics:
            lines.append(f"  {topic.title}: {topic.summary}")
            if topic.content_markdown:
                for content_line in topic.content_markdown.splitlines():
                    lines.append(f"    {content_line}")

    return "\n".join(lines)


def render_command_help(
    spec: CliSpec,
    command: CommandSpec,
) -> str:
    help_spec = command.help or command_help_defaults()
    usage = command.display_path
    if command.parameters:
        if help_spec.usage_options_token:
            usage += f" {help_spec.usage_options_token}"
        else:
            usage += f" [{help_spec.usage_options_label}]"
    lines: list[str] = [command.summary]
    if command.description:
        lines.extend(["", command.description])
    lines.extend(["", f"Usage: {usage}"])

    grouped_params = _group_parameters(spec, command)
    section_renderers = {
        "options": lambda: _render_options_section(grouped_params),
        "required_inputs": lambda: _render_grouped_params(grouped_params, ParameterGroup.REQUIRED_INPUTS),
        "conditional_inputs": lambda: _render_grouped_params(grouped_params, ParameterGroup.CONDITIONAL_INPUTS),
        "optional_inputs": lambda: _render_grouped_params(grouped_params, ParameterGroup.OPTIONAL_INPUTS),
        "input_sources": lambda: _render_grouped_params(grouped_params, ParameterGroup.INPUT_SOURCES),
        "execution_controls": lambda: _render_grouped_params(grouped_params, ParameterGroup.EXECUTION_CONTROLS),
        "advanced": lambda: _render_grouped_params(grouped_params, ParameterGroup.ADVANCED),
        "constraints": lambda: _render_constraints(command),
        "examples": lambda: _render_examples(command),
    }

    for section in help_spec.section_order:
        if section in {"usage", "summary"}:
            continue
        rendered = section_renderers.get(section, lambda: [])()
        if rendered:
            lines.extend(["", *rendered])

    return "\n".join(lines)


def render_group_help(group: GroupView) -> str:
    lines: list[str] = [group.summary]
    if group.description:
        lines.extend(["", group.description])
    lines.extend(["", f"Usage: {group.usage}"])

    if group.commands:
        lines.extend(["", "Commands:"])
        for entry in group.commands:
            lines.append(f"  {entry.name:<20} {entry.summary}")

    if group.use_cases:
        lines.extend(["", "Use Case:"])
        for command in group.use_cases:
            lines.append(f"  {command}")

    return "\n".join(lines)


def render_topic_help(topic: DocTopicSpec) -> str:
    lines = [
        f"Topic: {topic.id}",
        "",
        topic.title,
        "",
        topic.summary,
    ]
    if topic.content_markdown:
        lines.extend(["", topic.content_markdown])
    return "\n".join(lines)


def _group_parameters(spec: CliSpec, command: CommandSpec) -> dict[ParameterGroup, tuple[ParameterSpec, ...]]:
    grouped: dict[ParameterGroup, tuple[ParameterSpec, ...]] = {}
    all_groups = {}
    for param in command.parameters:
        all_groups.setdefault(param.group, []).append(param)
    return {group: tuple(params) for group, params in all_groups.items()}


def _render_grouped_params(
    grouped_params: dict[ParameterGroup, tuple[ParameterSpec, ...]],
    group: ParameterGroup,
) -> list[str]:
    params = grouped_params.get(group, ())
    if not params:
        return []
    lines = [f"{GROUP_TITLES[group]}:"]
    for param in params:
        lines.extend(_render_parameter_block(param, indent="  "))
    return lines


def _render_options_section(
    grouped_params: dict[ParameterGroup, tuple[ParameterSpec, ...]],
) -> list[str]:
    merged_groups = (
        ParameterGroup.REQUIRED_INPUTS,
        ParameterGroup.CONDITIONAL_INPUTS,
        ParameterGroup.OPTIONAL_INPUTS,
        ParameterGroup.INPUT_SOURCES,
        ParameterGroup.EXECUTION_CONTROLS,
    )
    params: list[ParameterSpec] = []
    for group in merged_groups:
        params.extend(grouped_params.get(group, ()))
    if not params:
        return []
    lines = ["Options:"]
    for param in params:
        lines.extend(_render_parameter_block(param, indent="  "))
    return lines


def _render_constraints(command: CommandSpec) -> list[str]:
    constraints = command.constraints
    if not constraints:
        return []
    param_by_id = {param.id: param for param in command.parameters}
    lines = ["Constraints:"]
    for constraint in constraints:
        severity = "warning" if constraint.severity == ConstraintSeverity.WARNING else "error"
        rule = _constraint_rule(constraint, param_by_id)
        line = f"  - {severity}: {rule}"
        note = _constraint_text(constraint)
        if note and note != rule:
            line += f" ({note})"
        lines.append(line)
    return lines


def _render_examples(command: CommandSpec) -> list[str]:
    if not command.examples:
        return []
    lines = ["Use Case:"]
    for example in command.examples:
        lines.append(f"  {example.command}")
    return lines


def _render_parameter_block(param: ParameterSpec, *, indent: str) -> list[str]:
    cli_names = ", ".join(param.cli_names)
    value_hint = _parameter_value_hint(param)
    line = f"{indent}{cli_names}"
    if value_hint:
        line += f" {value_hint}"
    meta_tokens = [_requiredness_label(param)]
    if _is_flag(param):
        meta_tokens.insert(0, "flag")
    default = _parameter_default(param)
    if default != "none":
        meta_tokens.append(f"default: {default}")
    if meta_tokens:
        line += f" ({', '.join(meta_tokens)})"
    line += f": {param.summary}"

    lines = [line]
    if param.requiredness.mode == RequirednessMode.CONDITIONAL and param.requiredness.message:
        lines.append(f"{indent}  when: {param.requiredness.message}")
    return lines


def _constraint_text(constraint: ConstraintSpec) -> str:
    return constraint.help_text or constraint.message


def command_help_defaults():
    from flow_xhs.cli.spec.model import CommandHelpSpec

    return CommandHelpSpec()


def _visible_global_options(spec: CliSpec) -> tuple[ParameterSpec, ...]:
    hidden_ids = {"help", "version"}
    return tuple(param for param in spec.global_options if param.id not in hidden_ids)


def _is_flag(param: ParameterSpec) -> bool:
    return param.kind.value == "flag"


def _requiredness_label(param: ParameterSpec) -> str:
    if param.group == ParameterGroup.REQUIRED_INPUTS:
        return "required"
    if param.group == ParameterGroup.CONDITIONAL_INPUTS:
        return "conditional"
    mode = param.requiredness.mode
    if mode == RequirednessMode.REQUIRED:
        return "required"
    if mode == RequirednessMode.CONDITIONAL:
        return "conditional"
    return "optional"


def _parameter_value_hint(param: ParameterSpec) -> str:
    if _is_flag(param):
        return ""
    if param.allowed_values:
        values = [value.value for value in param.allowed_values if not value.hidden]
        if values:
            return f"<{'|'.join(values)}>"
    kind = param.value_type.kind.value
    if kind == "string":
        if param.id == "input":
            return "<path|->"
        return "<string>"
    if kind == "integer":
        return "<int>"
    if kind == "number":
        return "<number>"
    if kind == "boolean":
        return "<true|false>"
    if kind == "path":
        return "<path>"
    if kind == "url":
        return "<url>"
    if kind == "timestamp":
        return "<rfc3339>"
    if kind == "array":
        item_kind = param.value_type.item_kind.value if param.value_type.item_kind else "value"
        if item_kind == "string":
            item_kind = "string"
        elif item_kind == "integer":
            item_kind = "int"
        return f"<{item_kind},...>"
    return "<value>"


def _constraint_rule(
    constraint: ConstraintSpec,
    param_by_id: dict[str, ParameterSpec],
) -> str:
    targets = [_constraint_target_label(target, param_by_id) for target in constraint.targets]
    kind = constraint.type.value
    if kind == "one_of":
        return f"one_of {' | '.join(targets)}"
    if kind == "when_then_requires" and constraint.condition and constraint.condition.op == "eq":
        field = constraint.condition.field or ""
        value = constraint.condition.value
        field_label = _constraint_target_label(field, param_by_id)
        required_targets = [target for target in targets if target != field_label]
        required_text = " + ".join(required_targets) if required_targets else "required target"
        return f"when {field_label}={value} -> requires {required_text}"
    if kind == "range" and len(constraint.targets) == 1:
        target_id = constraint.targets[0]
        target_label = _constraint_target_label(target_id, param_by_id)
        param = param_by_id.get(target_id)
        if param is not None and (param.value_type.min is not None or param.value_type.max is not None):
            lower = "-inf" if param.value_type.min is None else str(param.value_type.min)
            upper = "+inf" if param.value_type.max is None else str(param.value_type.max)
            return f"range {target_label} in [{lower}, {upper}]"
        return f"range {target_label}"
    return _constraint_text(constraint)


def _constraint_target_label(target_id: str, param_by_id: dict[str, ParameterSpec]) -> str:
    param = param_by_id.get(target_id)
    if param is None:
        return target_id
    return param.cli_names[0]


def _parameter_default(param: ParameterSpec) -> str:
    if param.default is None:
        return "none"
    if param.default.display_value:
        return param.default.display_value
    if param.default.value is None:
        return "none"
    return str(param.default.value)
