from m.core import yaml
from m.pydantic import KebabModel
from pydantic import BaseModel
from pydantic.fields import PydanticUndefined

from .actions import Action
from .misc import MetadataOutput

TEMPLATE = """\
# AUTOGENERATED FILE - Do not update by hand!
# Edit {py_path}/{module_name}.py
#   then run `m github actions [python_file]` to update

name: {name}
description: {description}

inputs:{inputs}
outputs:{outputs}
runs:
  using: composite
  steps:
    {steps}
"""
EMPTY_DICT = '{}'  # noqa: P103 - not a format string
InputInfo = dict[str, str | bool]  # container for pydantic model properties


class TemplateVars(BaseModel):
    """Template variables."""

    py_path: str

    module_name: str

    name: str

    description: str

    inputs: str

    outputs: str

    steps: str


def _indent(text: str, num: int) -> str:
    spaces = '  ' * num
    return text.replace('\n', f'\n{spaces}', -1)


class ActionFile(BaseModel):
    """Helper class to write the action file."""

    py_path: str

    module_name: str

    name: str

    description: str

    action: Action

    inputs: type[KebabModel]

    outputs: dict[str, MetadataOutput]

    python_path: str

    available_outputs: dict[str, str]

    def build_inputs(self: 'ActionFile') -> str:
        """Generate the inputs for the action.

        Returns:
            A string to add to the Github action.
        """
        all_inputs: dict[str, InputInfo] = {}
        for prop_name, prop_info in self.inputs.model_fields.items():
            input_info: InputInfo = {
                'description': prop_info.description or '',
                'required': prop_info.is_required(),
            }
            default_val = prop_info.get_default()
            if default_val is not PydanticUndefined:
                input_info['default'] = default_val
            all_inputs[prop_info.alias or prop_name] = input_info
        if not all_inputs:
            return f' {EMPTY_DICT}'
        str_inputs = _indent(yaml.dumps(all_inputs), 1)
        return f'\n  {str_inputs}'

    def build_outputs(
        self: 'ActionFile',
        outputs: dict[str, MetadataOutput],
    ) -> str:
        """Generate the outputs for the action.

        Args:
            outputs: The outputs to add to the action.

        Returns:
            A string to add to the Github action.
        """
        if not outputs:
            return f' {EMPTY_DICT}'
        raw_data = {
            output_name.replace('_', '-', -1): output_val.model_dump()
            for output_name, output_val in outputs.items()
        }
        str_outputs = _indent(yaml.dumps(raw_data), 1)
        return f'\n  {str_outputs}'

    def build_steps_str(self: 'ActionFile') -> str:
        """Generate a github action str for the build steps.

        Returns:
            A string to add to the Github action.
        """
        lines: list[str] = [
            step.to_str(self.python_path, self.available_outputs)
            for step in self.action.steps
        ]
        return _indent('\n'.join(lines), 2)

    def __str__(self: 'ActionFile') -> str:
        """Stringify the action file.

        Returns:
            The github action.
        """
        template_vars = TemplateVars(
            py_path=self.py_path,
            module_name=self.module_name.replace('.', '/', -1),
            name=self.name,
            description=self.description,
            inputs=self.build_inputs(),
            outputs=self.build_outputs(self.outputs),
            steps=self.build_steps_str(),
        )
        file_content = TEMPLATE.format(**template_vars.model_dump())
        return '\n'.join([
            line.rstrip()
            for line in file_content.split('\n')
        ])
