package atscfg

/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License 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.
 */

import (
	"errors"
	"path/filepath"
	"strings"

	"github.com/apache/trafficcontrol/v8/lib/go-tc"
)

// CfgMeta is a definition of the location of an arbitrary configuration file.
type CfgMeta struct {
	// Name is the basename of the file itself.
	Name string
	// Path is the absolute path to the directory containing the file.
	Path string
}

// ConfigFilesListOpts contains settings to configure generation options.
type ConfigFilesListOpts struct {
	// ATSMajorVersion is the integral major version of Apache Traffic server,
	// used to generate the proper config for the proper version.
	//
	// If omitted or 0, the major version will be read from the Server's Profile Parameter config file 'package' name 'trafficserver'. If no such Parameter exists, the ATS version will default to 5.
	// This was the old Traffic Control behavior, before the version was specifiable externally.
	//
	ATSMajorVersion uint
}

// MakeConfigFilesList returns the list of configuration files that need to be
// generated for the given server, any warnings, and any error.
func MakeConfigFilesList(
	configDir string,
	server *Server,
	serverParams []tc.ParameterV5,
	deliveryServices []DeliveryService,
	deliveryServiceServers []DeliveryServiceServer,
	globalParams []tc.ParameterV5,
	cacheGroupArr []tc.CacheGroupNullableV5,
	topologies []tc.TopologyV5,
	opt *ConfigFilesListOpts,
) ([]CfgMeta, []string, error) {
	if opt == nil {
		opt = &ConfigFilesListOpts{}
	}
	warnings := []string{}

	if server.CacheGroup == "" {
		return nil, warnings, errors.New("this server missing Cachegroup")
	} else if server.CacheGroupID == 0 {
		return nil, warnings, errors.New("this server missing CachegroupID")
	} else if server.TCPPort == nil {
		return nil, warnings, errors.New("server missing TCPPort")
	} else if server.HostName == "" {
		return nil, warnings, errors.New("server missing HostName")
	} else if server.CDNID == 0 {
		return nil, warnings, errors.New("server missing CDNID")
	} else if server.CDN == "" {
		return nil, warnings, errors.New("server missing CDNName")
	} else if server.ID == 0 {
		return nil, warnings, errors.New("server missing ID")
	} else if len(server.Profiles) == 0 {
		return nil, warnings, errors.New("server missing Profile")
	}

	atsMajorVersion := getATSMajorVersion(opt.ATSMajorVersion, serverParams, &warnings)

	dses, dsWarns := filterConfigFileDSes(server, deliveryServices, deliveryServiceServers)
	warnings = append(warnings, dsWarns...)

	locationParams := getLocationParams(serverParams)

	uriSignedDSes, signDSWarns := getURISignedDSes(dses)
	warnings = append(warnings, signDSWarns...)

	configFiles := []CfgMeta{}

	if locationParams["remap.config"].Path != "" {
		configLocation := locationParams["remap.config"].Path
		for _, ds := range uriSignedDSes {
			cfgName := "uri_signing_" + string(ds) + ".config"
			// If there's already a parameter for it, don't clobber it. The user may wish to override the location.
			if _, ok := locationParams[cfgName]; !ok {
				p := locationParams[cfgName]
				p.Name = cfgName
				p.Path = configLocation
				locationParams[cfgName] = p
			}
		}
	}

locationParamsFor:
	for cfgFile, cfgParams := range locationParams {
		if strings.HasSuffix(cfgFile, ".config") {
			dsConfigFilePrefixes := []string{
				"hdr_rw_mid_", // must come before hdr_rw_, to avoid thinking we have a "hdr_rw_" with a ds of "mid_x"
				"hdr_rw_",
				"regex_remap_",
				"url_sig_",
				"uri_signing_",
			}
		prefixFor:
			for _, prefix := range dsConfigFilePrefixes {
				if strings.HasPrefix(cfgFile, prefix) {
					dsName := strings.TrimSuffix(strings.TrimPrefix(cfgFile, prefix), ".config")
					if _, ok := dses[tc.DeliveryServiceName(dsName)]; !ok {
						warnings = append(warnings, "server profile had 'location' Parameter '"+cfgFile+"', but delivery Service '"+dsName+"' is not assigned to this Server! Not including in meta config!")
						continue locationParamsFor
					}
					break prefixFor // if it has a prefix, don't check the next prefix. This is important for hdr_rw_mid_, which will match hdr_rw_ and result in a "ds name" of "mid_x" if we don't continue here.
				}
			}
		}

		atsCfg := CfgMeta{
			Name: cfgParams.Name,
			Path: cfgParams.Path,
		}

		configFiles = append(configFiles, atsCfg)
	}

	configFiles, configDirWarns, err := addMetaObjConfigDir(configFiles, configDir, server, locationParams, uriSignedDSes, dses, cacheGroupArr, topologies, atsMajorVersion)
	warnings = append(warnings, configDirWarns...)
	return configFiles, warnings, err
}

// addMetaObjConfigDir takes the Meta Object generated from TO data, and the ATS config directory
// and prepends the config directory to all relative paths,
// and creates MetaObj entries for all required config files which have no location parameter.
// If configDir is empty and any location Parameters have relative paths, returns an error.
// Returns the amended config files list, any warnings, and any error.
func addMetaObjConfigDir(
	configFiles []CfgMeta,
	configDir string,
	server *Server,
	locationParams map[string]configProfileParams, // map[configFile]params; 'location' and 'URL' Parameters on serverHostName's Profile
	uriSignedDSes []tc.DeliveryServiceName,
	dses map[tc.DeliveryServiceName]DeliveryService,
	cacheGroupArr []tc.CacheGroupNullableV5,
	topologies []tc.TopologyV5,
	atsMajorVersion uint,
) ([]CfgMeta, []string, error) {
	warnings := []string{}

	if server.CacheGroup == "" {
		return nil, warnings, errors.New("server missing Cachegroup")
	}

	cacheGroups, err := makeCGMap(cacheGroupArr)
	if err != nil {
		return nil, warnings, errors.New("making CG map: " + err.Error())
	}

	// Note there may be multiple files with the same name in different directories.
	configFilesM := map[string][]CfgMeta{} // map[fileShortName]CfgMeta
	for _, fi := range configFiles {
		configFilesM[fi.Name] = append(configFilesM[fi.Path], fi)
	}

	// add all strictly required files, all of which should be in the base config directory.
	// If they don't exist, create them.
	// If they exist with a relative path, prepend configDir.
	// If any exist with a relative path, or don't exist, and configDir is empty, return an error.
	for _, fileName := range requiredFiles(atsMajorVersion) {
		if _, ok := configFilesM[fileName]; ok {
			continue
		}
		if configDir == "" {
			return nil, warnings, errors.New("required file '" + fileName + "' has no location Parameter, and ATS config directory not found.")
		}
		configFilesM[fileName] = []CfgMeta{{
			Name: fileName,
			Path: configDir,
		}}
	}

	for fileName, fis := range configFilesM {
		newFis := []CfgMeta{}
		for _, fi := range fis {
			if !filepath.IsAbs(fi.Path) {
				if configDir == "" {
					return nil, warnings, errors.New("file '" + fileName + "' has location Parameter with relative path '" + fi.Path + "', but ATS config directory was not found.")
				}
				absPath := filepath.Join(configDir, fi.Path)
				fi.Path = absPath
			}
			newFis = append(newFis, fi)
		}
		configFilesM[fileName] = newFis
	}

	nameTopologies := makeTopologyNameMap(topologies)

	for _, ds := range dses {
		if ds.XMLID == "" {
			warnings = append(warnings, "got Delivery Service with nil XMLID - not considering!")
			continue
		}

		err := error(nil)
		// Note we log errors, but don't return them.
		// If an individual DS has an error, we don't want to break the rest of the CDN.
		if ds.Topology != nil && *ds.Topology != "" {
			topology := nameTopologies[TopologyName(*ds.Topology)]

			placement, err := getTopologyPlacement(tc.CacheGroupName(server.CacheGroup), topology, cacheGroups, &ds)
			if err != nil {
				return nil, warnings, errors.New("getting topology placement: " + err.Error())
			}
			if placement.IsFirstCacheTier {
				if (ds.FirstHeaderRewrite != nil && *ds.FirstHeaderRewrite != "") || ds.MaxOriginConnections != nil || ds.ServiceCategory != nil {
					fileName := FirstHeaderRewriteConfigFileName(ds.XMLID)
					if configFilesM, err = ensureConfigFile(configFilesM, fileName, configDir); err != nil {
						warnings = append(warnings, "ensuring config file '"+fileName+"': "+err.Error())
					}
				}
			}
			if placement.IsInnerCacheTier {
				if (ds.InnerHeaderRewrite != nil && *ds.InnerHeaderRewrite != "") || ds.MaxOriginConnections != nil || ds.ServiceCategory != nil {
					fileName := InnerHeaderRewriteConfigFileName(ds.XMLID)
					if configFilesM, err = ensureConfigFile(configFilesM, fileName, configDir); err != nil {
						warnings = append(warnings, "ensuring config file '"+fileName+"': "+err.Error())
					}
				}
			}
			if placement.IsLastCacheTier {
				if (ds.LastHeaderRewrite != nil && *ds.LastHeaderRewrite != "") || ds.MaxOriginConnections != nil || ds.ServiceCategory != nil {
					fileName := LastHeaderRewriteConfigFileName(ds.XMLID)
					if configFilesM, err = ensureConfigFile(configFilesM, fileName, configDir); err != nil {
						warnings = append(warnings, "ensuring config file '"+fileName+"': "+err.Error())
					}
				}
			}
		} else if strings.HasPrefix(server.Type, tc.EdgeTypePrefix) {
			if (ds.EdgeHeaderRewrite != nil || ds.MaxOriginConnections != nil || ds.ServiceCategory != nil) &&
				strings.HasPrefix(server.Type, tc.EdgeTypePrefix) {
				fileName := "hdr_rw_" + ds.XMLID + ".config"
				if configFilesM, err = ensureConfigFile(configFilesM, fileName, configDir); err != nil {
					warnings = append(warnings, "ensuring config file '"+fileName+"': "+err.Error())
				}
			}
		} else if strings.HasPrefix(server.Type, tc.MidTypePrefix) {
			if (ds.MidHeaderRewrite != nil || ds.MaxOriginConnections != nil || ds.ServiceCategory != nil) &&
				ds.Type != nil && tc.DSType(*ds.Type).UsesMidCache() &&
				strings.HasPrefix(server.Type, tc.MidTypePrefix) {
				fileName := "hdr_rw_mid_" + ds.XMLID + ".config"
				if configFilesM, err = ensureConfigFile(configFilesM, fileName, configDir); err != nil {
					warnings = append(warnings, "ensuring config file '"+fileName+"': "+err.Error())
				}
			}
		}
		if ds.RegexRemap != nil {
			configFile := "regex_remap_" + ds.XMLID + ".config"
			if configFilesM, err = ensureConfigFile(configFilesM, configFile, configDir); err != nil {
				warnings = append(warnings, "ensuring config file '"+configFile+"': "+err.Error())
			}
		}
		if ds.SigningAlgorithm != nil && *ds.SigningAlgorithm == tc.SigningAlgorithmURLSig {
			configFile := "url_sig_" + ds.XMLID + ".config"
			if configFilesM, err = ensureConfigFile(configFilesM, configFile, configDir); err != nil {
				warnings = append(warnings, "ensuring config file '"+configFile+"': "+err.Error())
			}
		}
		if ds.SigningAlgorithm != nil && *ds.SigningAlgorithm == tc.SigningAlgorithmURISigning {
			configFile := "uri_signing_" + ds.XMLID + ".config"
			if configFilesM, err = ensureConfigFile(configFilesM, configFile, configDir); err != nil {
				warnings = append(warnings, "ensuring config file '"+configFile+"': "+err.Error())
			}
		}
	}

	newFiles := []CfgMeta{}
	for _, fis := range configFilesM {
		for _, fi := range fis {
			newFiles = append(newFiles, fi)
		}
	}
	return newFiles, warnings, nil
}

// getURISignedDSes returns the URI-signed Delivery Services, and any warnings.
func getURISignedDSes(dses map[tc.DeliveryServiceName]DeliveryService) ([]tc.DeliveryServiceName, []string) {
	warnings := []string{}

	uriSignedDSes := []tc.DeliveryServiceName{}
	for _, ds := range dses {
		if ds.ID == nil {
			warnings = append(warnings, "got delivery service with no id, skipping!")
			continue
		}
		if ds.XMLID == "" {
			warnings = append(warnings, "got delivery service with no xmlId (name), skipping!")
			continue
		}
		if _, ok := dses[tc.DeliveryServiceName(ds.XMLID)]; !ok {
			continue // skip: this ds isn't assigned to this server, this is normal
		}
		if ds.SigningAlgorithm == nil || *ds.SigningAlgorithm != tc.SigningAlgorithmURISigning {
			continue // not signed, so not in our list of signed dses to make config files for.
		}
		uriSignedDSes = append(uriSignedDSes, tc.DeliveryServiceName(ds.XMLID))
	}

	return uriSignedDSes, warnings
}

// filterConfigFileDSes returns the DSes that should have config files for the given server.
// Returns the delivery services and any warnings.
func filterConfigFileDSes(server *Server, deliveryServices []DeliveryService, deliveryServiceServers []DeliveryServiceServer) (map[tc.DeliveryServiceName]DeliveryService, []string) {
	warnings := []string{}

	dses := map[tc.DeliveryServiceName]DeliveryService{}

	if tc.CacheTypeFromString(server.Type) != tc.CacheTypeMid {
		dsIDs := map[int]struct{}{}
		for _, ds := range deliveryServices {
			if ds.ID == nil {
				warnings = append(warnings, "got delivery service with no ID, skipping!")
				continue
			}
			if ds.Active == tc.DSActiveStateInactive {
				continue
			}
			dsIDs[*ds.ID] = struct{}{}
		}

		// TODO verify?
		//		serverIDs := []int{server.ID}

		dssMap := map[int]struct{}{}
		for _, dss := range deliveryServiceServers {
			if dss.Server != server.ID {
				continue
			}
			if _, ok := dsIDs[dss.DeliveryService]; !ok {
				continue
			}
			dssMap[dss.DeliveryService] = struct{}{}
		}

		for _, ds := range deliveryServices {
			if ds.ID == nil {
				warnings = append(warnings, "got deliveryservice with nil id, skipping!")
				continue
			}
			if ds.XMLID == "" {
				warnings = append(warnings, "got deliveryservice with nil xmlId (name), skipping!")
				continue
			}
			if _, ok := dssMap[*ds.ID]; !ok && ds.Topology == nil {
				continue
			}
			dses[tc.DeliveryServiceName(ds.XMLID)] = ds
		}
	} else {
		for _, ds := range deliveryServices {
			if ds.ID == nil {
				warnings = append(warnings, "got deliveryservice with nil id, skipping!")
				continue
			}
			if ds.XMLID == "" {
				warnings = append(warnings, "got deliveryservice with nil xmlId (name), skipping!")
				continue
			}
			if ds.CDNID != server.CDNID {
				continue
			}
			if ds.Active == tc.DSActiveStateInactive {
				continue
			}
			dses[tc.DeliveryServiceName(ds.XMLID)] = ds
		}
	}
	return dses, warnings
}

// getTOURLAndReverseProxy returns the toURL and toReverseProxyURL if they exist, or empty strings if they don't.
func getTOURLAndReverseProxy(globalParams []tc.Parameter) (string, string) {
	toReverseProxyURL := ""
	toURL := ""
	for _, param := range globalParams {
		if param.Name == "tm.rev_proxy.url" {
			toReverseProxyURL = param.Value
		} else if param.Name == "tm.url" {
			toURL = param.Value
		}
		if toReverseProxyURL != "" && toURL != "" {
			break
		}
	}
	return toURL, toReverseProxyURL
}

func getLocationParams(serverParams []tc.ParameterV5) map[string]configProfileParams {
	locationParams := map[string]configProfileParams{}
	for _, param := range serverParams {
		if param.Name == "location" {
			p := locationParams[param.ConfigFile]
			p.Name = param.ConfigFile
			p.Path = param.Value
			locationParams[param.ConfigFile] = p
		}
	}
	return locationParams
}

type configProfileParams struct {
	Name string
	Path string
}

func requiredFiles(atsMajorVersion uint) []string {
	if atsMajorVersion >= 9 {
		return requiredFiles9()
	}
	return requiredFiles8()
}

// requiredFiles8 the list of config files required by ATS 8.
// Note these are not exhaustive. This is only used to error if these are missing.
// The presence of these is no guarantee the location Parameters are complete and correct.
func requiredFiles8() []string {
	return []string{
		"cache.config",
		"hosting.config",
		"ip_allow.config",
		"parent.config",
		"plugin.config",
		"records.config",
		"remap.config",
		"ssl_server_name.yaml",
		"storage.config",
		"volume.config",
	}
}

// requiredFiles9 is the list of config files required by ATS 9.
// Note these are not exhaustive. This is only used to error if these are missing.
// The presence of these is no guarantee the location Parameters are complete and correct.
func requiredFiles9() []string {
	return []string{
		"cache.config",
		"hosting.config",
		"ip_allow.yaml",
		"parent.config",
		"plugin.config",
		"records.config",
		"remap.config",
		"sni.yaml",
		"storage.config",
		"volume.config",
		"strategies.yaml",
	}
}

// ensureConfigFile ensures files contains the given fileName. If so, returns files unmodified.
// If not, if configDir is empty, returns an error.
// If not, and configDir is nonempty, creates the given file, configDir location, and returns files.
func ensureConfigFile(files map[string][]CfgMeta, fileName string, configDir string) (map[string][]CfgMeta, error) {
	if _, ok := files[fileName]; ok {
		return files, nil
	}
	if configDir == "" {
		return files, errors.New("required file '" + fileName + "' has no location Parameter, and ATS config directory not found.")
	}
	files[fileName] = []CfgMeta{{
		Name: fileName,
		Path: configDir,
	}}
	return files, nil
}
