// Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License"). You may
// not use this file except in compliance with the License. A copy of the
// License is located at
//
//     http://aws.amazon.com/apache2.0/
//
// or in the "license" file accompanying this file. This file is distributed
// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
// express or implied. See the License for the specific language governing
// permissions and limitations under the License.

package handlers

import (
	"log"
	"net/http"
	"sort"
	"strings"

	"github.com/aws/amazon-ec2-metadata-mock/pkg/mock/dynamic"
	"github.com/aws/amazon-ec2-metadata-mock/pkg/mock/static"
	"github.com/aws/amazon-ec2-metadata-mock/pkg/mock/userdata"
	"github.com/aws/amazon-ec2-metadata-mock/pkg/server"
)

const (
	// shortestRouteLength represents smallest metadata path without prefix + "/" ex: "mac/"
	shortestRouteLength = 4
	versionsPath        = "/"
	latestPath          = "/latest"
)

var (
	routeLookupTable = make(map[string][]string)

	supportedVersions   = []string{"latest"}
	supportedCategories = []string{"dynamic", "meta-data", "user-data"}

	// trimmedRoutes represents the list of routes served by the http server without "latest/meta-data/" prefix
	trimmedRoutes         []string
	trimmedRoutesDynamic  []string
	trimmedRoutesUserdata []string
)

// CatchAllHandler returns subpath listings, if available; 404 status code otherwise
func CatchAllHandler(res http.ResponseWriter, req *http.Request) {
	log.Println("Received request to CatchAllHandler: ", req.URL.Path)
	// CatchAllHandler may be invoked before ListRoutesHandler
	if len(trimmedRoutes) == 0 {
		formatRoutes()
	}

	var routes []string

	// Clean request path and determine which route list to search
	trimmedRoute := req.URL.Path
	if strings.HasPrefix(trimmedRoute, static.ServicePath) {
		trimmedRoute = strings.TrimPrefix(trimmedRoute, static.ServicePath+"/")
		log.Println("static prefix detected..trimming: ", trimmedRoute)
		routes = trimmedRoutes
	} else if strings.HasPrefix(trimmedRoute, dynamic.ServicePath) {
		trimmedRoute = strings.TrimPrefix(trimmedRoute, dynamic.ServicePath+"/")
		log.Println("dynamic prefix detected..trimming: ", trimmedRoute)
		routes = trimmedRoutesDynamic
	} else if strings.HasPrefix(trimmedRoute, userdata.ServicePath) {
		trimmedRoute = strings.TrimPrefix(trimmedRoute, userdata.ServicePath+"/")
		log.Println("userdata prefix detected..trimming: ", trimmedRoute)
		routes = trimmedRoutesUserdata
	} else {
		server.ReturnNotFoundResponse(res)
		return
	}

	if paths, ok := routeLookupTable[trimmedRoute]; ok {
		log.Printf("CatchAllHandler entry %s already in map: %v \n", trimmedRoute, routeLookupTable[trimmedRoute])
		server.FormatAndReturnTextResponse(res, strings.Join(paths, "\n"))
		return
	}

	/*
		The request /latest/meta-data/iam will populate results as [info, security-credentials/]
		Note: not every path for which iam is a prefix needs to be appended to results.
		ex: iam/security-credentials/baskinc-role should not be added because security-credentials/ already exists
		results are cached in routeLookupTable
	*/
	resultSet := map[string]bool{}
	for _, route := range routes {
		if strings.Contains(route, trimmedRoute) {
			// ex: iam/security-credentials contains iam
			route = strings.TrimPrefix(route, trimmedRoute+"/")
			// route is now security-credentials
			route = trimRoute(route)
			// store unique route
			resultSet[route] = true
			log.Printf("adding route: %s to results\n", route)
		}
	}

	if len(resultSet) <= 0 {
		server.ReturnNotFoundResponse(res)
		return
	}

	results := make([]string, 0, len(resultSet))
	for key := range resultSet {
		results = append(results, key)
	}
	sort.Strings(results)

	routeLookupTable[trimmedRoute] = results
	log.Printf("CatchAllHandler: adding  %s  and its routes: %v to the map\n", trimmedRoute, results)
	server.FormatAndReturnTextResponse(res, strings.Join(results, "\n"))
	return
}

// ListRoutesHandler returns the list of supported paths
func ListRoutesHandler(res http.ResponseWriter, req *http.Request) {
	log.Println("Received request to display paths: ", req.URL.Path)
	// Routes are not available until runtime; only want to do this ONCE
	if len(trimmedRoutes) == 0 {
		formatRoutes()
	}

	// these paths do not use routeLookupTable due to inconsistency of trailing "/" with IMDS
	switch req.URL.Path {
	case userdata.ServicePath:
		server.FormatAndReturnOctetResponse(res, strings.Join(trimmedRoutesUserdata, "\n")+"\n")
	case static.ServicePath:
		server.FormatAndReturnTextResponse(res, strings.Join(trimAndSortRoutes(trimmedRoutes), "\n")+"\n")
	case dynamic.ServicePath:
		server.FormatAndReturnTextResponse(res, strings.Join(trimAndSortRoutes(trimmedRoutesDynamic), "\n")+"\n")
	case latestPath:
		server.FormatAndReturnTextResponse(res, strings.Join(supportedCategories, "\n")+"\n")
	case versionsPath:
		server.FormatAndReturnTextResponse(res, strings.Join(supportedVersions, "\n")+"\n")
	default:
		server.ReturnNotFoundResponse(res)
	}
	return
}

func formatRoutes() {
	var trimmedRoute string
	for _, route := range server.Routes {
		if strings.HasPrefix(route, dynamic.ServicePath) {
			// Omit /latest/dynamic and /latest/user-data
			trimmedRoute = strings.TrimPrefix(route, dynamic.ServicePath)
			// Omit empty paths and "/"
			if len(trimmedRoute) >= shortestRouteLength {
				trimmedRoute = strings.TrimPrefix(trimmedRoute, "/")
				trimmedRoutesDynamic = append(trimmedRoutesDynamic, trimmedRoute)
			}
		} else if strings.HasPrefix(route, userdata.ServicePath) {
			// Omit /latest/dynamic and /latest/meta-data
			trimmedRoute = strings.TrimPrefix(route, userdata.ServicePath)
			// Omit empty paths and "/"
			if len(trimmedRoute) >= shortestRouteLength {
				trimmedRoute = strings.TrimPrefix(trimmedRoute, "/")
				trimmedRoutesUserdata = append(trimmedRoutesUserdata, trimmedRoute)
			}

		} else if strings.HasPrefix(route, static.ServicePath) {
			// Omit /latest/meta-data and /latest/user-data
			trimmedRoute = strings.TrimPrefix(route, static.ServicePath)
			// Omit empty paths and "/"
			if len(trimmedRoute) >= shortestRouteLength {
				trimmedRoute = strings.TrimPrefix(trimmedRoute, "/")
				trimmedRoutes = append(trimmedRoutes, trimmedRoute)
			}
		}
	}
	sort.Sort(sort.StringSlice(trimmedRoutes))
	sort.Sort(sort.StringSlice(trimmedRoutesDynamic))
	sort.Sort(sort.StringSlice(trimmedRoutesUserdata))
}

func trimRoute(route string) string {
	// Remove trailing path elements, e.g. "0e:49:61:0f:c3:11/device-number" => "0e:49:61:0f:c3:11"
	route, _, foundSlash := stringCut(route, "/")
	if !foundSlash {
		return route
	}
	return route + "/"
}

// stringCut is a backfill for `strings.Cut` in Go <1.18.
func stringCut(s, sep string) (before, after string, found bool) {
	if i := strings.Index(s, sep); i > -1 {
		return s[:i], s[i+len(sep):], true
	}
	return s, "", false
}

func trimAndSortRoutes(notTrimmedRoutes []string) []string {
	trimmedRouteSet := map[string]bool{}
	for _, route := range notTrimmedRoutes {
		trimmedRouteSet[trimRoute(route)] = true
	}

	trimmedRoutes := make([]string, 0, len(trimmedRouteSet))
	for key := range trimmedRouteSet {
		trimmedRoutes = append(trimmedRoutes, key)
	}
	sort.Strings(trimmedRoutes)

	return trimmedRoutes
}
