package middlewares

import (
	"encoding/json"
	"fmt"
	"strconv"
	"strings"

	"github.com/getkin/kin-openapi/openapi2"
	"github.com/getkin/kin-openapi/openapi3"
	"github.com/ghodss/yaml"
	"github.com/sirupsen/logrus"
	"github.com/snyk/driftctl/pkg/resource"
	"github.com/snyk/driftctl/pkg/resource/aws"
)

// Explodes the body attribute of api gateway apis v1|v2 to dedicated resources as per Terraform documentations
// (https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/api_gateway_rest_api)
// (https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/apigatewayv2_api)
type AwsApiGatewayApiExpander struct {
	resourceFactory resource.ResourceFactory
}

type OpenAPIAwsExtensions struct {
	GatewayResponses map[string]interface{} `json:"x-amazon-apigateway-gateway-responses"`
}

type OpenAPIAwsMethodExtensions struct {
	Integration map[string]interface{} `json:"x-amazon-apigateway-integration"`
}

func NewAwsApiGatewayApiExpander(resourceFactory resource.ResourceFactory) AwsApiGatewayApiExpander {
	return AwsApiGatewayApiExpander{
		resourceFactory: resourceFactory,
	}
}

func (m AwsApiGatewayApiExpander) Execute(remoteResources, resourcesFromState *[]*resource.Resource) error {
	newStateResources := make([]*resource.Resource, 0)
	for _, res := range *resourcesFromState {
		// Ignore all resources other than aws_api_gateway_rest_api && aws_apigatewayv2_api
		if res.ResourceType() != aws.AwsApiGatewayRestApiResourceType &&
			res.ResourceType() != aws.AwsApiGatewayV2ApiResourceType {
			newStateResources = append(newStateResources, res)
			continue
		}

		newStateResources = append(newStateResources, res)

		err := m.handleBody(res, &newStateResources, remoteResources)
		if err != nil {
			return err
		}
	}
	*resourcesFromState = newStateResources
	return nil
}

func (m *AwsApiGatewayApiExpander) handleBody(api *resource.Resource, results, remoteResources *[]*resource.Resource) error {
	body := api.Attrs.GetString("body")
	if body == nil || *body == "" {
		return nil
	}

	docV3 := &openapi3.T{}
	if err := json.Unmarshal([]byte(*body), &docV3); err != nil {
		if _, ok := err.(*json.SyntaxError); ok {
			err = yaml.Unmarshal([]byte(*body), &docV3)
		}
		if err != nil {
			return err
		}
	}
	// It's an OpenAPI v3 document
	if docV3.OpenAPI != "" {
		return m.handleBodyOpenAPIv3(api, docV3, results, remoteResources)
	}

	docV2 := &openapi2.T{}
	if err := json.Unmarshal([]byte(*body), &docV2); err != nil {
		if _, ok := err.(*json.SyntaxError); ok {
			err = yaml.Unmarshal([]byte(*body), &docV2)
		}
		if err != nil {
			return err
		}
	}
	// It's an OpenAPI v2 document
	if docV2.Swagger != "" {
		return m.handleBodyOpenAPIv2(api, docV2, results, remoteResources)
	}

	return nil
}

func (m *AwsApiGatewayApiExpander) handleBodyOpenAPIv3(api *resource.Resource, doc *openapi3.T, results, remoteResources *[]*resource.Resource) error {
	if api.ResourceType() == aws.AwsApiGatewayV2ApiResourceType {
		return m.handleBodyOpenAPIv3GatewayV2(api, doc, results, remoteResources)
	}

	apiId := api.ResourceId()
	for path, pathItem := range doc.Paths {
		if res := m.createApiGatewayResource(apiId, path, results, remoteResources); res != nil {
			ops := pathItem.Operations()
			for httpMethod, method := range ops {
				m.createApiGatewayMethod(apiId, res.ResourceId(), httpMethod, results)
				for statusCode := range method.Responses {
					m.createApiGatewayMethodResponse(apiId, res.ResourceId(), httpMethod, statusCode, results)
				}
				m.createApiGatewayIntegration(apiId, res.ResourceId(), httpMethod, results)
				if err := m.createMethodExtensionsResources(apiId, res.ResourceId(), httpMethod, method.Extensions, results); err != nil {
					return nil
				}
			}
		}
	}
	if err := m.createExtensionsResources(apiId, doc.Extensions, results); err != nil {
		return nil
	}
	return nil
}

func (m *AwsApiGatewayApiExpander) handleBodyOpenAPIv3GatewayV2(api *resource.Resource, doc *openapi3.T, results, remoteResources *[]*resource.Resource) error {
	for path, pathValue := range doc.Paths {
		for method := range doc.Paths[path].Operations() {
			openAPIDerivedRoute := findMatchingOpenAPIDerivedRoute(api.ResourceId(), path, method, remoteResources)
			if openAPIDerivedRoute != nil {
				dummy := m.resourceFactory.CreateAbstractResource(
					aws.AwsApiGatewayV2RouteResourceType,
					openAPIDerivedRoute.ResourceId(),
					map[string]interface{}{},
				)
				*results = append(*results, dummy)
			}

			for _, operation := range pathValue.Operations() {
				integ, err := decodeMethodExtensions(operation.Extensions)
				if err != nil {
					continue
				}

				openAPIDerivedIntegration := findMatchingOpenAPIDerivedIntegration(api.ResourceId(),
					integ,
					remoteResources)
				if openAPIDerivedIntegration != nil {
					dummy := m.resourceFactory.CreateAbstractResource(
						aws.AwsApiGatewayV2IntegrationResourceType,
						openAPIDerivedIntegration.ResourceId(),
						map[string]interface{}{},
					)
					*results = append(*results, dummy)
				}
			}
		}
	}
	return nil
}

// The types are similar structurally between the openapi2 and openapi3
// libraries, but without generics we can't really de-dup this witout code
// generation, which isn't worth it for this short function.
func (m *AwsApiGatewayApiExpander) handleBodyOpenAPIv2GatewayV2(api *resource.Resource, doc *openapi2.T, results, remoteResources *[]*resource.Resource) error {
	for path, pathValue := range doc.Paths {
		for method := range doc.Paths[path].Operations() {
			openAPIDerivedRoute := findMatchingOpenAPIDerivedRoute(api.ResourceId(), path, method, remoteResources)
			if openAPIDerivedRoute != nil {
				dummy := m.resourceFactory.CreateAbstractResource(
					aws.AwsApiGatewayV2RouteResourceType,
					openAPIDerivedRoute.ResourceId(),
					map[string]interface{}{},
				)
				*results = append(*results, dummy)
			}

			for _, operation := range pathValue.Operations() {
				integ, err := decodeMethodExtensions(operation.Extensions)
				if err != nil {
					continue
				}

				openAPIDerivedIntegration := findMatchingOpenAPIDerivedIntegration(api.ResourceId(),
					integ,
					remoteResources)
				if openAPIDerivedIntegration != nil {
					dummy := m.resourceFactory.CreateAbstractResource(
						aws.AwsApiGatewayV2IntegrationResourceType,
						openAPIDerivedIntegration.ResourceId(),
						map[string]interface{}{},
					)
					*results = append(*results, dummy)
				}
			}
		}
	}
	return nil
}

func findMatchingOpenAPIDerivedRoute(desiredApiID, desiredPath, desiredMethod string, remoteResources *[]*resource.Resource) *resource.Resource {
	desiredRouteKey := fmt.Sprintf("%s %s", desiredMethod, desiredPath)
	for _, remoteResource := range *remoteResources {
		if remoteResource.ResourceType() != aws.AwsApiGatewayV2RouteResourceType {
			continue
		}
		routeKey := *remoteResource.Attributes().GetString("route_key")
		apiID := *remoteResource.Attributes().GetString("api_id")
		if desiredApiID == apiID && routeKey == desiredRouteKey {
			return remoteResource
		}
	}
	return nil
}

func findMatchingOpenAPIDerivedIntegration(desiredApiID string, desiredIntegration *OpenAPIAwsMethodExtensions, remoteResources *[]*resource.Resource) *resource.Resource {
	desiredType := desiredIntegration.Integration["type"]
	desiredMethod := desiredIntegration.Integration["httpMethod"]

	if desiredType == nil || desiredMethod == nil {
		return nil
	}

	for _, remoteResource := range *remoteResources {
		if remoteResource.ResourceType() != aws.AwsApiGatewayV2IntegrationResourceType {
			continue
		}
		apiID := *remoteResource.Attributes().GetString("api_id")
		integrationType := *remoteResource.Attributes().GetString("integration_type")
		if remoteResource.Attributes().GetString("integration_method") == nil {
			// This is nilable in MOCK type only, and they cannot be embedded
			continue
		}
		integrationMethod := *remoteResource.Attributes().GetString("integration_method")
		if desiredApiID == apiID && integrationType == desiredType && integrationMethod == desiredMethod {
			return remoteResource
		}
	}
	return nil
}

func (m *AwsApiGatewayApiExpander) handleBodyOpenAPIv2(api *resource.Resource, doc *openapi2.T, results, remoteResources *[]*resource.Resource) error {
	if api.ResourceType() == aws.AwsApiGatewayV2ApiResourceType {
		return m.handleBodyOpenAPIv2GatewayV2(api, doc, results, remoteResources)
	}

	apiId := api.ResourceId()
	for path, pathItem := range doc.Paths {
		if res := m.createApiGatewayResource(apiId, path, results, remoteResources); res != nil {
			ops := pathItem.Operations()
			for httpMethod, method := range ops {
				m.createApiGatewayMethod(apiId, res.ResourceId(), httpMethod, results)
				for statusCode := range method.Responses {
					m.createApiGatewayMethodResponse(apiId, res.ResourceId(), httpMethod, statusCode, results)
				}
				m.createApiGatewayIntegration(apiId, res.ResourceId(), httpMethod, results)
				if err := m.createMethodExtensionsResources(apiId, res.ResourceId(), httpMethod, method.Extensions, results); err != nil {
					return nil
				}
			}
		}
	}
	if err := m.createExtensionsResources(apiId, doc.Extensions, results); err != nil {
		return nil
	}
	return nil
}

// Create resources based on our OpenAPIAwsExtensions struct
func (m *AwsApiGatewayApiExpander) createExtensionsResources(apiId string, extensions map[string]interface{}, results *[]*resource.Resource) error {
	ext, err := decodeExtensions(extensions)
	if err != nil {
		logrus.WithFields(logrus.Fields{
			"id":   apiId,
			"type": aws.AwsApiGatewayRestApiResourceType,
		}).Debug("Failed to decode extensions from the OpenAPI body attribute")
		return err
	}
	for gtwResponse := range ext.GatewayResponses {
		m.createApiGatewayGatewayResponse(apiId, gtwResponse, results)
	}
	return nil
}

// Create resources based on our OpenAPIAwsMethodExtensions struct
func (m *AwsApiGatewayApiExpander) createMethodExtensionsResources(apiId, resourceId, httpMethod string, extensions map[string]interface{}, results *[]*resource.Resource) error {
	ext, err := decodeMethodExtensions(extensions)
	if err != nil {
		logrus.WithFields(logrus.Fields{
			"id":   apiId,
			"type": aws.AwsApiGatewayRestApiResourceType,
		}).Debug("Failed to decode method extensions from the OpenAPI body attribute")
		return err
	}
	if responses, exist := ext.Integration["responses"]; exist {
		for _, response := range responses.(map[string]interface{}) {
			if statusCode, ok := response.(map[string]interface{})["statusCode"]; ok {
				if s, isFloat64 := statusCode.(float64); isFloat64 {
					statusCode = strconv.FormatFloat(s, 'f', -1, 64)
				}
				m.createApiGatewayIntegrationResponse(apiId, resourceId, httpMethod, statusCode.(string), results)
			}
		}
	}
	return nil
}

// Create aws_api_gateway_resource resource
func (m *AwsApiGatewayApiExpander) createApiGatewayResource(apiId, path string, results, remoteResources *[]*resource.Resource) *resource.Resource {
	if res := foundMatchingResource(apiId, path, remoteResources); res != nil {
		newResource := m.resourceFactory.CreateAbstractResource(aws.AwsApiGatewayResourceResourceType, res.ResourceId(), map[string]interface{}{
			"rest_api_id": *res.Attributes().GetString("rest_api_id"),
			"path":        path,
		})
		*results = append(*results, newResource)
		return newResource
	}
	return nil
}

// Create aws_api_gateway_gateway_response resource
func (m *AwsApiGatewayApiExpander) createApiGatewayGatewayResponse(apiId, gtwResponse string, results *[]*resource.Resource) {
	newResource := m.resourceFactory.CreateAbstractResource(
		aws.AwsApiGatewayGatewayResponseResourceType,
		strings.Join([]string{"aggr", apiId, gtwResponse}, "-"),
		map[string]interface{}{},
	)
	*results = append(*results, newResource)
}

// Returns the aws_api_gateway_resource resource that matches the path attribute
func foundMatchingResource(apiId, path string, remoteResources *[]*resource.Resource) *resource.Resource {
	for _, res := range *remoteResources {
		if res.ResourceType() == aws.AwsApiGatewayResourceResourceType {
			p := res.Attributes().GetString("path")
			i := res.Attributes().GetString("rest_api_id")
			if p != nil && i != nil && *p == path && *i == apiId {
				return res
			}
		}
	}
	return nil
}

// Create aws_api_gateway_method resource
func (m *AwsApiGatewayApiExpander) createApiGatewayMethod(apiId, resourceId, httpMethod string, results *[]*resource.Resource) {
	newResource := m.resourceFactory.CreateAbstractResource(
		aws.AwsApiGatewayMethodResourceType,
		strings.Join([]string{"agm", apiId, resourceId, httpMethod}, "-"),
		map[string]interface{}{},
	)
	*results = append(*results, newResource)
}

// Create aws_api_gateway_method_response resource
func (m *AwsApiGatewayApiExpander) createApiGatewayMethodResponse(apiId, resourceId, httpMethod, statusCode string, results *[]*resource.Resource) {
	newResource := m.resourceFactory.CreateAbstractResource(
		aws.AwsApiGatewayMethodResponseResourceType,
		strings.Join([]string{"agmr", apiId, resourceId, httpMethod, statusCode}, "-"),
		map[string]interface{}{},
	)
	*results = append(*results, newResource)
}

// Decode openapi.Extensions into our custom OpenAPIAwsExtensions struct that follows AWS
// OpenAPI addons.
func decodeExtensions(extensions map[string]interface{}) (*OpenAPIAwsExtensions, error) {
	rawExtensions, err := json.Marshal(extensions)
	if err != nil {
		return nil, err
	}
	decodedExtensions := &OpenAPIAwsExtensions{}
	err = json.Unmarshal(rawExtensions, decodedExtensions)
	if err != nil {
		return nil, err
	}
	return decodedExtensions, nil
}

// Create aws_api_gateway_integration resource
func (m *AwsApiGatewayApiExpander) createApiGatewayIntegration(apiId, resourceId, httpMethod string, results *[]*resource.Resource) {
	newResource := m.resourceFactory.CreateAbstractResource(
		aws.AwsApiGatewayIntegrationResourceType,
		strings.Join([]string{"agi", apiId, resourceId, httpMethod}, "-"),
		map[string]interface{}{},
	)
	*results = append(*results, newResource)
}

// Create aws_api_gateway_integration resource
func (m *AwsApiGatewayApiExpander) createApiGatewayIntegrationResponse(apiId, resourceId, httpMethod, statusCode string, results *[]*resource.Resource) {
	newResource := m.resourceFactory.CreateAbstractResource(
		aws.AwsApiGatewayIntegrationResponseResourceType,
		strings.Join([]string{"agir", apiId, resourceId, httpMethod, statusCode}, "-"),
		map[string]interface{}{},
	)
	*results = append(*results, newResource)
}

// Decode openapi.Method.Extensions into our custom OpenAPIAwsMethodExtensions struct that follows AWS
// OpenAPI addons.
func decodeMethodExtensions(extensions map[string]interface{}) (*OpenAPIAwsMethodExtensions, error) {
	rawExtensions, err := json.Marshal(extensions)
	if err != nil {
		return nil, err
	}
	decodedExtensions := &OpenAPIAwsMethodExtensions{}
	err = json.Unmarshal(rawExtensions, decodedExtensions)
	if err != nil {
		return nil, err
	}
	return decodedExtensions, nil
}
