import keyword

from data_types import DataType, ObjectField, is_primitive_python_type
from operations import Operation


def escape_escapes(s: str) -> str:
    return s.replace("\\", "\\\\")


def docstring_comment(lines: list[str]) -> list[str]:
    if len(lines) == 0:
        return []
    if len(lines) == 1:
        return [f'"""{escape_escapes(lines[0])}"""']
    result = ['"""' + escape_escapes(lines[0])]
    result += [escape_escapes(line) for line in lines[1:]]
    result += ['"""']
    return result


def indent(lines: list[str], level: int) -> list[str]:
    """Indent each line.

    :param lines: Lines of text to be indented
    :param level: Level of indent (each indent level is four spaces)
    :return: Same lines of text provided after each line is indented
    """
    if level == 0:
        return lines
    else:
        return ["    " * level + line if line else "" for line in lines]


def data_type(d_type: DataType) -> list[str]:
    """Generate Python code defining the provided data type.

    :param d_type: Parsed API data type to render into Python code
    :return: Lines of Python code defining the provided data type
    """
    docstring_lines = (
        docstring_comment(d_type.description.split("\n")) if d_type.description else []
    )
    lines = []

    if d_type.enum_values:
        if any(str(t) in keyword.kwlist for v, t in d_type.enum_values.items()):
            lines.append(f"{d_type.name} = {d_type.python_type}")
            docstring_lines = (
                d_type.description.split("\n") if d_type.description else []
            )
            docstring_lines += ["", "Acceptable values:"] + [
                "* " + str(v) for v in d_type.enum_values
            ]
            lines.extend(docstring_comment(docstring_lines))
        else:
            lines.append(f"class {d_type.name}({d_type.python_type}, Enum):")
            lines.extend(indent(docstring_lines, 1))
            if docstring_lines:
                lines.append("")

            lines.extend(
                indent([f'{t} = "{v}"' for v, t in d_type.enum_values.items()], 1)
            )
    elif is_primitive_python_type(d_type.python_type):
        lines.append(f"{d_type.name} = {d_type.python_type}")
        lines.extend(docstring_lines)
    elif d_type.python_type == "ImplicitDict":
        if any(f.api_name in keyword.kwlist for f in d_type.fields):
            lines.append(f"{d_type.name} = dict")
            if d_type.description:
                docstring_lines = d_type.description.split("\n")
                docstring_lines.append("")
            else:
                docstring_lines = []
            docstring_lines.append("Expected keys:")
            docstring_lines.extend("* " + f.api_name for f in d_type.fields)
            lines.extend(docstring_comment(docstring_lines))
            lines.append("")
        else:
            lines.append(f"class {d_type.name}(ImplicitDict):")
            lines.extend(indent(docstring_lines, 1))
            if docstring_lines:
                lines.append("")

            for field in d_type.fields:
                lines.extend(indent(_object_field(field), 1))
                lines.append("")
            if d_type.fields:
                lines.pop()
    else:
        lines.append(f"{d_type.name} = {d_type.python_type}")

    return lines


def _object_field(field: ObjectField) -> list[str]:
    """Generate an unindented definition of the provided field in Python code.

    :param field: Data type field to render into Python code
    :return: Lines of Python code defining the provided field
    """
    d_type = field.python_type if field.required else f"Optional[{field.python_type}]"
    if field.default is not None:
        if field.literal_default:
            default_suffix = f" = {field.default}"
        elif isinstance(field.default, str):
            default_suffix = f' = "{field.default}"'
        else:
            default_suffix = f" = {str(field.default)}"
    else:
        default_suffix = ""
    lines = [f"{field.api_name}: {d_type}{default_suffix}"]
    if field.description:
        lines.extend(docstring_comment(field.description.split("\n")))
    return lines


def header(d_types: list[DataType]) -> list[str]:
    lines = ["# This file is autogenerated; do not modify manually!", ""]
    lines.extend(["from __future__ import annotations", ""])

    lines.append("from enum import Enum")

    basic_types = ["Dict"]
    if any(d.python_type.startswith("List[") for d in d_types) or any(
        any(f.python_type.startswith("List[") for f in d.fields) for d in d_types
    ):
        basic_types.append("List")
    if any(any(not f.required for f in d.fields) for d in d_types):
        basic_types.append("Optional")
    lines.append("from typing import " + ", ".join(basic_types))
    lines.append("")

    lines.append("from uas_standards.utils import Operation")
    lines.append("")

    lines.append("from implicitdict import ImplicitDict")
    if any(("StringBasedDateTime" in d.python_type) for d in d_types) or any(
        any(("StringBasedDateTime" in f.python_type) for f in d.fields) for d in d_types
    ):
        lines[-1] = lines[-1] + ", StringBasedDateTime"

    return lines


def data_types(d_types: list[DataType], default_package: str) -> list[str]:
    already_defined = [kw for kw in keyword.kwlist]
    already_defined += [
        "int",
        "float",
        "complex",
        "str",
        "list",
        "tuple",
        "range",
        "bytes",
        "bytearray",
        "memoryview",
        "dict",
        "bool",
        "set",
        "frozenset",
        "StringBasedDateTime",
        "ImplicitDict",
    ]

    lines = []

    # Declare types in dependency order
    total_defined = 0
    n_defined = 1
    not_defined = []

    def _core_type(type_name):
        core_type = type_name
        while "[" in core_type:
            core_type = core_type[core_type.index("[") + 1 :]
        while "]" in core_type:
            core_type = core_type[0 : core_type.index("]")]
        return core_type

    while n_defined > 0:
        n_defined = 0
        for d_type in d_types:
            if d_type.name in already_defined:
                continue
            if _core_type(d_type.python_type) not in already_defined:
                continue
            if any(
                (_core_type(f.python_type) not in already_defined)
                for f in d_type.fields
            ):
                continue

            lines.extend(["", ""])
            lines.extend(data_type(d_type))
            already_defined.append(d_type.name)
            n_defined += 1
            total_defined += 1

        not_defined = [
            d_type for d_type in d_types if d_type.name not in already_defined
        ]
        if not not_defined:
            break

        # Declare certain types external
        if n_defined == 0:
            remaining_names = {
                d_type.name for d_type in d_types if d_type.name not in already_defined
            }
            for d_type in d_types:
                only_external_undefined_fields = True
                for f in d_type.fields:
                    core_field_type = _core_type(f.python_type)
                    if (
                        core_field_type not in already_defined
                        and core_field_type in remaining_names
                    ):
                        only_external_undefined_fields = False
                        break
                if only_external_undefined_fields:
                    for f in d_type.fields:
                        core_field_type = _core_type(f.python_type)
                        if core_field_type not in already_defined:
                            lines.extend(["", ""])
                            lines.append(
                                f"from {default_package} import {core_field_type}"
                            )
                            already_defined.append(core_field_type)
                            n_defined += 1
                    break

    if not_defined:
        not_defined_list = "; ".join(
            [
                t.name
                + " ("
                + ", ".join(
                    _core_type(f.python_type)
                    for f in t.fields
                    if _core_type(f.python_type) not in already_defined
                )
                + ")"
                for t in not_defined
            ]
        )
        raise RuntimeError(f"Failed to define data types: {not_defined_list}")

    lines.append("")
    return lines


def operations(ops: list[Operation]) -> list[str]:
    lines = ["class OperationID(str, Enum):"]
    for op in ops:
        lines.append(f'    {op.name} = "{op.id}"')
    lines.append("")
    lines.append("")

    lines.append("OPERATIONS: Dict[OperationID, Operation] = {")
    for op in ops:
        lines.append(f"    OperationID.{op.name}: Operation(")
        lines.append(f'        id="{op.id}",')
        lines.append(f'        path="{op.path}",')
        lines.append(f'        verb="{op.verb}",')
        lines.append(f"        request_body_type={op.request_body_type},")
        lines.append("        response_body_type={")
        for code, resp_body in op.response_body_type.items():
            lines.append(f"            {code}: {resp_body},")
        lines.append("        }")
        lines.append("    ),")
    lines.append("}")

    return lines


def api_version(spec: dict) -> list[str]:
    version = spec.get("info", {}).get("version", None)
    if version:
        result = [f'API_VERSION = "{version}"']
    else:
        result = ["API_VERSION = None"]
    name = spec.get("info", {}).get("title", "unknown")
    result.append(
        f'"""Version of {name} OpenAPI specification from which the objects in this package were generated."""'
    )
    return result
