from __future__ import annotations

import argparse
import json
from copy import deepcopy
from pathlib import Path
from typing import Any, Callable, Dict, List, Optional, Type, Union

from samtranslator.compat import pydantic
from samtranslator.internal.schema_source import (
    any_cfn_resource,
    aws_serverless_api,
    aws_serverless_application,
    aws_serverless_connector,
    aws_serverless_function,
    aws_serverless_graphqlapi,
    aws_serverless_httpapi,
    aws_serverless_layerversion,
    aws_serverless_simpletable,
    aws_serverless_statemachine,
)
from samtranslator.internal.schema_source.common import BaseModel, LenientBaseModel


class Globals(BaseModel):
    Function: Optional[aws_serverless_function.Globals]
    Api: Optional[aws_serverless_api.Globals]
    HttpApi: Optional[aws_serverless_httpapi.Globals]
    SimpleTable: Optional[aws_serverless_simpletable.Globals]
    StateMachine: Optional[aws_serverless_statemachine.Globals]
    LayerVersion: Optional[aws_serverless_layerversion.Globals]


Resources = Union[
    aws_serverless_connector.Resource,
    aws_serverless_function.Resource,
    aws_serverless_simpletable.Resource,
    aws_serverless_statemachine.Resource,
    aws_serverless_layerversion.Resource,
    aws_serverless_api.Resource,
    aws_serverless_httpapi.Resource,
    aws_serverless_application.Resource,
    aws_serverless_graphqlapi.Resource,
]


class _ModelWithoutResources(LenientBaseModel):
    Globals: Optional[Globals]


class SamModel(_ModelWithoutResources):
    Resources: Dict[
        str,
        Union[
            Resources,
            # Ignore resources that are not AWS::Serverless::*
            any_cfn_resource.Resource,
        ],
    ]


class Model(_ModelWithoutResources):
    Resources: Dict[str, Resources]


def get_schema(model: Type[pydantic.BaseModel]) -> Dict[str, Any]:
    obj = model.schema()

    # http://json-schema.org/understanding-json-schema/reference/schema.html#schema
    # https://github.com/pydantic/pydantic/issues/1478
    # Validated in https://github.com/aws/serverless-application-model/blob/5c82f5d2ae95adabc9827398fba8ccfc3dbe101a/tests/schema/test_validate_schema.py#L91
    obj["$schema"] = "http://json-schema.org/draft-04/schema#"

    # Pydantic automatically adds title to model (https://github.com/pydantic/pydantic/issues/1051),
    # and the YAML extension for VS Code then shows 'PassThroughProp' as title for pass-through
    # properties (instead of the title of the property itself)... so manually deleting it.
    del obj["definitions"]["PassThroughProp"]["title"]

    return obj


def json_dumps(obj: Any) -> str:
    return json.dumps(obj, indent=2, sort_keys=True) + "\n"


def _replace_in_dict(d: Dict[str, Any], keyword: str, replace: Callable[[Dict[str, Any]], Any]) -> Dict[str, Any]:
    """
    Replace any dict containing keyword.

    replace() takes the containing dict as input, and returns its replacement.
    """
    if keyword in d:
        d = replace(d)
    for k, v in d.items():
        if isinstance(v, dict):
            d[k] = _replace_in_dict(v, keyword, replace)
    return d


def _deep_get(d: Dict[str, Any], path: List[str]) -> Dict[str, Any]:
    """
    Returns value at path defined by the keys in `path`.
    """
    for k in path:
        d = d[k]
    return d


def _add_embedded_connectors(schema: Dict[str, Any]) -> None:
    """
    Add embedded Connectors resource attribute to supported CloudFormation resources.
    """
    # We get the definition from an existing SAM resource
    embedded_connector = schema["definitions"][
        "samtranslator__internal__schema_source__aws_serverless_function__Resource"
    ]["properties"]["Connectors"]

    profiles = json.loads(Path("samtranslator/model/connector_profiles/profiles.json").read_text())

    # Only add the resource attributes to resources that support it
    source_resources = profiles["Permissions"].keys()
    for resource in source_resources:
        schema["definitions"][resource]["properties"]["Connectors"] = embedded_connector


def extend_with_cfn_schema(sam_schema: Dict[str, Any], cfn_schema: Dict[str, Any]) -> None:
    """
    Add CloudFormation resources and template syntax to SAM schema.
    """

    sam_defs = sam_schema["definitions"]
    cfn_defs = cfn_schema["definitions"]

    sam_props = sam_schema["properties"]
    cfn_props = cfn_schema["properties"]

    # Add Resources from CloudFormation schema to SAM schema
    cfn_resources = cfn_props["Resources"]["patternProperties"]["^[a-zA-Z0-9]+$"]["anyOf"]
    sam_props["Resources"]["additionalProperties"]["anyOf"].extend(cfn_resources)

    # Add any other top-level properties from CloudFormation schema to SAM schema
    for k in cfn_props:
        if k not in sam_props:
            sam_props[k] = cfn_props[k]

    # Add definitions from CloudFormation schema to SAM schema
    for k in cfn_defs:
        if k in sam_defs:
            raise Exception(f"Key {k} already in SAM schema definitions")
        sam_defs[k] = cfn_defs[k]

    _add_embedded_connectors(sam_schema)

    # Inject CloudFormation documentation to SAM pass-through properties
    def replace_passthrough(d: Dict[str, Any]) -> Dict[str, Any]:
        passthrough = d["__samPassThrough"]
        schema = deepcopy(_deep_get(cfn_schema, passthrough["schemaPath"]))
        schema["markdownDescription"] = passthrough["markdownDescriptionOverride"]
        schema["title"] = d["title"]  # Still want the original title, CFN property name could be different
        return schema

    _replace_in_dict(
        sam_schema,
        "__samPassThrough",
        replace_passthrough,
    )

    # The unified schema should include all supported properties
    sam_schema["additionalProperties"] = False


def main() -> None:
    parser = argparse.ArgumentParser()
    parser.add_argument("--cfn-schema", help="input CloudFormation schema", type=Path, required=True)
    parser.add_argument("--sam-schema", help="output SAM schema", type=Path, required=True)
    parser.add_argument("--unified-schema", help="output unified schema", type=Path, required=True)
    args = parser.parse_args()

    sam_schema = get_schema(SamModel)
    args.sam_schema.write_text(json_dumps(sam_schema))

    unified_schema = get_schema(Model)
    cfn_schema = json.loads(args.cfn_schema.read_text())
    extend_with_cfn_schema(unified_schema, cfn_schema)
    args.unified_schema.write_text(json_dumps(unified_schema))


if __name__ == "__main__":
    main()
