package cdn

/*
 * 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 (
	"context"
	"database/sql"
	"errors"
	"fmt"
	"math"
	"net/http"
	"runtime"
	"strconv"
	"strings"
	"sync"
	"time"

	"github.com/apache/trafficcontrol/v8/lib/go-log"
	"github.com/apache/trafficcontrol/v8/lib/go-tc"
	"github.com/apache/trafficcontrol/v8/traffic_ops/traffic_ops_golang/api"
	"github.com/apache/trafficcontrol/v8/traffic_ops/traffic_ops_golang/dbhelpers"
	"github.com/apache/trafficcontrol/v8/traffic_ops/traffic_ops_golang/deliveryservice"
	"github.com/apache/trafficcontrol/v8/traffic_ops/traffic_ops_golang/trafficvault"
)

const (
	CDNDNSSECKeyType     = "dnssec"
	DNSSECStatusExisting = "existing"

	DNSSECGenerationCPURatio = 0.66
)

func CreateDNSSECKeys(w http.ResponseWriter, r *http.Request) {
	inf, userErr, sysErr, errCode := api.NewInfo(r, nil, nil)
	if userErr != nil || sysErr != nil {
		api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr)
		return
	}
	defer inf.Close()

	if !inf.Config.TrafficVaultEnabled {
		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, errors.New("deleting CDN DNSSEC keys from Traffic Vault: Traffic Vault is not configured"))
		return
	}

	req := tc.CDNDNSSECGenerateReq{}
	if err := api.Parse(r.Body, inf.Tx.Tx, &req); err != nil {
		api.HandleErr(w, r, inf.Tx.Tx, http.StatusBadRequest, errors.New("parsing request: "+err.Error()), nil)
		return
	}
	if req.EffectiveDateUnix == nil {
		now := tc.CDNDNSSECGenerateReqDate(time.Now().Unix())
		req.EffectiveDateUnix = &now
	}
	cdnName := *req.Key

	cdnID, ok, err := getCDNIDFromName(inf.Tx.Tx, tc.CDNName(cdnName))
	if err != nil {
		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, errors.New("getting cdn ID from name '"+cdnName+"': "+err.Error()))
		return
	} else if !ok {
		api.HandleErr(w, r, inf.Tx.Tx, http.StatusNotFound, nil, nil)
		return
	}

	cdnDomain, cdnExists, err := dbhelpers.GetCDNDomainFromName(inf.Tx.Tx, tc.CDNName(cdnName))
	if err != nil {
		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, errors.New("create DNSSEC keys: getting CDN domain: "+err.Error()))
		return
	} else if !cdnExists {
		api.HandleErr(w, r, inf.Tx.Tx, http.StatusNotFound, errors.New("cdn '"+cdnName+"' not found"), nil)
		return
	}
	userErr, sysErr, statusCode := dbhelpers.CheckIfCurrentUserCanModifyCDN(inf.Tx.Tx, cdnName, inf.User.UserName)
	if userErr != nil || sysErr != nil {
		api.HandleErr(w, r, inf.Tx.Tx, statusCode, userErr, sysErr)
		return
	}
	if err := generateStoreDNSSECKeys(inf.Tx.Tx, cdnName, cdnDomain, uint64(*req.TTL), uint64(*req.KSKExpirationDays), uint64(*req.ZSKExpirationDays), int64(*req.EffectiveDateUnix), inf.Vault, r.Context()); err != nil {
		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, errors.New("generating and storing DNSSEC CDN keys: "+err.Error()))
		return
	}
	// NOTE: using a separate transaction (with its own timeout) for the changelog because the main
	// transaction can time out if DNSSEC generation takes too long
	db, err := api.GetDB(r.Context())
	if err != nil {
		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, errors.New("generating CDN DNSSEC keys: getting DB from request context for changelog: "+err.Error()))
		return
	}
	logCtx, logCancel := context.WithTimeout(r.Context(), 30*time.Second)
	defer logCancel()
	logTx, err := db.BeginTxx(logCtx, nil)
	if err != nil {
		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, errors.New("generating CDN DNSSEC keys: could not begin transaction for changelog: "+err.Error()))
		return
	}
	defer func() {
		if err := logTx.Commit(); err != nil && err != sql.ErrTxDone {
			log.Errorln("generating CDN DNSSEC keys: committing transaction for changelog: " + err.Error())
		}
	}()
	api.CreateChangeLogRawTx(api.ApiChange, "CDN: "+cdnName+", ID: "+strconv.Itoa(cdnID)+", ACTION: Generated DNSSEC keys", inf.User, logTx.Tx)
	api.WriteResp(w, r, "Successfully created dnssec keys for "+cdnName)
}

// DefaultDSTTL is the default DS Record TTL to use, if no CDN Snapshot exists, or if no tld.ttls.DS parameter exists.
// This MUST be the same value as Traffic Router's default. Currently:
// traffic_router/core/src/main/java/org/apache/traffic_control/traffic_router/core/dns/SignatureManager.java:476
// `final Long dsTtl = ZoneUtils.getLong(config.get("ttls"), "DS", 60);`.
// If Traffic Router and Traffic Ops differ, and a user is using the default, errors may occur!
// Users are advised to set the tld.ttls.DS CRConfig.json Parameter, so the default is not used!
// Traffic Ops functions SHOULD warn whenever this default is used.
const DefaultDSTTL = 60 * time.Second

func GetDNSSECKeys(w http.ResponseWriter, r *http.Request) {
	inf, userErr, sysErr, errCode := api.NewInfo(r, []string{"name"}, nil)
	if userErr != nil || sysErr != nil {
		api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr)
		return
	}
	defer inf.Close()

	if !inf.Config.TrafficVaultEnabled {
		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, errors.New("deleting CDN DNSSEC keys from Traffic Vault: Traffic Vault is not configured"))
		return
	}

	cdnName := inf.Params["name"]

	tvKeys, keysExist, err := inf.Vault.GetDNSSECKeys(cdnName, inf.Tx.Tx, r.Context())
	if err != nil {
		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, errors.New("getting DNSSEC CDN keys: "+err.Error()))
		return
	}
	if !keysExist {
		// TODO emulates Perl; change to error, 404?
		api.WriteRespAlertObj(w, r, tc.SuccessLevel, " - Dnssec keys for "+cdnName+" could not be found. ", struct{}{}) // emulates Perl
		return
	}

	dsTTL, err := GetDSRecordTTL(inf.Tx.Tx, cdnName)
	if err != nil {
		log.Errorln("Getting DNSSEC Keys: getting DS Record TTL from CRConfig Snapshot: " + err.Error())
		log.Errorf("Getting DNSSEC Keys: getting DS Record TTL failed, using default %v. It is STRONGLY ADVISED to fix the error, and ensure a CRConfig Snapshot exists for the CDN, and a tld.ttls.DS CRConfig.json Parameter exists on a Router Profile on the CDN. Default DS Records may cause unexpected behavior or errors!\n", DefaultDSTTL)
		dsTTL = DefaultDSTTL
	}

	keys, err := deliveryservice.MakeDNSSECKeysFromTrafficVaultKeys(tvKeys, dsTTL)
	if err != nil {
		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, errors.New("creating DNSSEC keys object from Traffic Vault keys: "+err.Error()))
		return
	}
	api.WriteResp(w, r, keys)
}

func GetDSRecordTTL(tx *sql.Tx, cdn string) (time.Duration, error) {
	ttlSeconds := 0
	if err := tx.QueryRow(`SELECT JSON_EXTRACT_PATH_TEXT(crconfig, 'config', 'ttls', 'DS') FROM snapshot WHERE cdn = $1`, cdn).Scan(&ttlSeconds); err != nil {
		return 0, errors.New("getting cdn '" + cdn + "' DS Record TTL from CRConfig: " + err.Error())
	}
	return time.Duration(ttlSeconds) * time.Second, nil
}

func generateStoreDNSSECKeys(
	tx *sql.Tx,
	cdnName string,
	cdnDomain string,
	ttlSeconds uint64,
	kExpDays uint64,
	zExpDays uint64,
	effectiveDateUnix int64,
	tv trafficvault.TrafficVault,
	ctx context.Context,
) error {

	zExp := time.Duration(zExpDays) * time.Hour * 24
	kExp := time.Duration(kExpDays) * time.Hour * 24
	ttl := time.Duration(ttlSeconds) * time.Second

	oldKeys, oldKeysExist, err := tv.GetDNSSECKeys(cdnName, tx, ctx)
	if err != nil {
		return errors.New("getting old dnssec keys: " + err.Error())
	}

	dses, err := GetCDNDeliveryServices(tx, cdnName)
	if err != nil {
		return errors.New("getting cdn delivery services: " + err.Error())
	}

	cdnDNSDomain := cdnDomain
	if !strings.HasSuffix(cdnDNSDomain, ".") {
		cdnDNSDomain = cdnDNSDomain + "."
	}
	cdnDNSDomain = strings.ToLower(cdnDNSDomain)

	inception := time.Now()
	newCDNZSK, err := deliveryservice.GetDNSSECKeysV11(tc.DNSSECZSKType, cdnDNSDomain, ttl, inception, inception.Add(zExp), tc.DNSSECKeyStatusNew, time.Unix(effectiveDateUnix, 0), false)
	if err != nil {
		return errors.New("creating zsk for cdn: " + err.Error())
	}

	newCDNKSK, err := deliveryservice.GetDNSSECKeysV11(tc.DNSSECKSKType, cdnDNSDomain, ttl, inception, inception.Add(kExp), tc.DNSSECKeyStatusNew, time.Unix(effectiveDateUnix, 0), true)
	if err != nil {
		return errors.New("creating ksk for cdn: " + err.Error())
	}

	newCDNZSKs := []tc.DNSSECKeyV11{newCDNZSK}
	newCDNKSKs := []tc.DNSSECKeyV11{newCDNKSK}

	if oldKeysExist {
		oldKeyCDN, oldKeyCDNExists := oldKeys[cdnName]
		if oldKeyCDNExists && len(oldKeyCDN.KSK) > 0 {
			ksk := oldKeyCDN.KSK[0]
			ksk.Status = DNSSECStatusExisting
			ksk.TTLSeconds = uint64(ttl / time.Second)
			ksk.ExpirationDateUnix = effectiveDateUnix
			newCDNKSKs = append(newCDNKSKs, ksk)
		}
		if oldKeyCDNExists && len(oldKeyCDN.ZSK) > 0 {
			zsk := oldKeyCDN.ZSK[0]
			zsk.Status = DNSSECStatusExisting
			zsk.TTLSeconds = uint64(ttl / time.Second)
			zsk.ExpirationDateUnix = effectiveDateUnix
			newCDNZSKs = append(newCDNZSKs, zsk)
		}
	}

	newKeys := tc.DNSSECKeysTrafficVault{}
	newKeys[cdnName] = tc.DNSSECKeySetV11{ZSK: newCDNZSKs, KSK: newCDNKSKs}

	cdnKeys := newKeys[cdnName]

	dsNames := []string{}
	for _, ds := range dses {
		dsNames = append(dsNames, ds.Name)
	}
	matchLists, err := deliveryservice.GetDeliveryServicesMatchLists(dsNames, tx)
	if err != nil {
		return errors.New("getting delivery service matchlists: " + err.Error())
	}

	jobList := make([]dnssecGenJob, 0, len(dses))
	for _, ds := range dses {
		if !ds.Type.IsHTTP() && !ds.Type.IsDNS() {
			continue // skip delivery services that aren't DNS or HTTP (e.g. ANY_MAP)
		}

		matchlist, ok := matchLists[ds.Name]
		if !ok {
			return errors.New("no regex match list found for delivery service '" + ds.Name)
		}

		exampleURLs := deliveryservice.MakeExampleURLs(ds.Protocol, ds.Type, ds.RoutingName, matchlist, cdnDomain)
		jobList = append(jobList, dnssecGenJob{
			XMLID:       ds.Name,
			ExampleURLs: exampleURLs,
			CDNKeys:     cdnKeys,
			KExp:        kExp,
			ZExp:        zExp,
			TTL:         ttl,
			OverrideTTL: true,
		})
	}
	numWorkers := int(math.Max(1, math.Floor(float64(runtime.NumCPU())*DNSSECGenerationCPURatio)))
	jobChan := make(chan dnssecGenJob, len(jobList))
	resultChan := make(chan dnssecGenResult, len(jobList))
	panickedChan := make(chan struct{}, numWorkers)
	wg := sync.WaitGroup{}
	wg.Add(numWorkers)
	for w := 0; w < numWorkers; w++ {
		go dnssecGenWorker(w, &wg, jobChan, resultChan, panickedChan)
	}
	for _, j := range jobList {
		jobChan <- j
	}
	close(jobChan)
	wg.Wait()
	select {
	case <-panickedChan:
		return errors.New("creating DNSSEC keys, at least one worker goroutine panicked")
	default:
		log.Infoln("no DNSSEC generation worker goroutines panicked")
	}
	for i := 0; i < len(jobList); i++ {
		res := <-resultChan
		if res.Error != nil {
			return fmt.Errorf("creating DNSSEC keys for delivery service %s: %s", res.XMLID, res.Error.Error())
		}
		newKeys[res.XMLID] = *res.Keys
	}

	if err := tv.PutDNSSECKeys(cdnName, newKeys, tx, ctx); err != nil {
		return errors.New("putting CDN DNSSEC keys in Traffic Vault: " + err.Error())
	}
	return nil
}

func dnssecGenWorker(id int, waitGroup *sync.WaitGroup, jobs <-chan dnssecGenJob, results chan<- dnssecGenResult, panicked chan<- struct{}) {
	log.Infof("DNSSEC gen worker %d starting", id)
	defer func() {
		if r := recover(); r != nil {
			panicked <- struct{}{}
			log.Errorf("DNSSEC gen worker %d recovered from panic: %v", id, r)
		}
		waitGroup.Done()
		log.Infof("DNSSEC gen worker %d exiting", id)
	}()
	for j := range jobs {
		log.Infof("DNSSEC gen worker %d creating keys for %s", id, j.XMLID)
		res := dnssecGenResult{XMLID: j.XMLID}
		dsKeys, err := deliveryservice.CreateDNSSECKeys(j.ExampleURLs, j.CDNKeys, j.KExp, j.ZExp, j.TTL, j.OverrideTTL)
		if err != nil {
			res.Error = err
		} else {
			res.Keys = &dsKeys
		}
		results <- res
	}
}

type dnssecGenJob struct {
	XMLID       string
	ExampleURLs []string
	CDNKeys     tc.DNSSECKeySetV11
	KExp        time.Duration
	ZExp        time.Duration
	TTL         time.Duration
	OverrideTTL bool
}

type dnssecGenResult struct {
	XMLID string
	Keys  *tc.DNSSECKeySetV11
	Error error
}

const API_DNSSECKEYS = "DELETE /cdns/name/:name/dnsseckeys"

type CDNDS struct {
	Name        string
	Protocol    *int
	Type        tc.DSType
	RoutingName string
}

// getCDNDeliveryServices returns basic data for the delivery services on the given CDN, as well as the CDN name, or any error.
func GetCDNDeliveryServices(tx *sql.Tx, cdn string) ([]CDNDS, error) {
	q := `
SELECT ds.xml_id, ds.protocol, t.name as type, ds.routing_name
FROM deliveryservice as ds
JOIN cdn ON ds.cdn_id = cdn.id
JOIN type as t ON ds.type = t.id
WHERE cdn.name = $1
`
	rows, err := tx.Query(q, cdn)
	if err != nil {
		return nil, errors.New("getting cdn delivery services: " + err.Error())
	}
	defer rows.Close()
	dses := []CDNDS{}
	for rows.Next() {
		ds := CDNDS{}
		dsTypeStr := ""
		if err := rows.Scan(&ds.Name, &ds.Protocol, &dsTypeStr, &ds.RoutingName); err != nil {
			return nil, errors.New("scanning cdn delivery services: " + err.Error())
		}
		dsType := tc.DSTypeFromString(dsTypeStr)
		if dsType == tc.DSTypeInvalid {
			return nil, errors.New("got invalid delivery service type '" + dsTypeStr + "'")
		}
		ds.Type = dsType
		dses = append(dses, ds)
	}
	return dses, nil
}

func DeleteDNSSECKeys(w http.ResponseWriter, r *http.Request) {
	inf, userErr, sysErr, errCode := api.NewInfo(r, []string{"name"}, nil)
	if userErr != nil || sysErr != nil {
		api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr)
		return
	}
	defer inf.Close()

	if !inf.Config.TrafficVaultEnabled {
		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, errors.New("deleting CDN DNSSEC keys from Traffic Vault: Traffic Vault is not configured"))
		return
	}

	key := inf.Params["name"]
	cdnID, ok, err := getCDNIDFromName(inf.Tx.Tx, tc.CDNName(key))
	if err != nil {
		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, errors.New("getting cdn id: "+err.Error()))
		return
	} else if !ok {
		api.HandleErr(w, r, inf.Tx.Tx, http.StatusNotFound, nil, nil)
		return
	}
	userErr, sysErr, statusCode := dbhelpers.CheckIfCurrentUserCanModifyCDN(inf.Tx.Tx, key, inf.User.UserName)
	if userErr != nil || sysErr != nil {
		api.HandleErr(w, r, inf.Tx.Tx, statusCode, userErr, sysErr)
		return
	}
	if err := inf.Vault.DeleteDNSSECKeys(key, inf.Tx.Tx, r.Context()); err != nil {
		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, errors.New("deleting CDN DNSSEC keys: "+err.Error()))
		return
	}

	api.CreateChangeLogRawTx(api.ApiChange, "CDN: "+key+", ID: "+strconv.Itoa(cdnID)+", ACTION: Deleted DNSSEC keys", inf.User, inf.Tx.Tx)
	successMsg := "Successfully deleted " + CDNDNSSECKeyType + " for " + key
	api.WriteResp(w, r, successMsg)
}

// getCDNIDFromName returns the CDN's ID if a CDN with the given name exists
func getCDNIDFromName(tx *sql.Tx, name tc.CDNName) (int, bool, error) {
	id := 0
	if err := tx.QueryRow(`SELECT id FROM cdn WHERE name = $1`, name).Scan(&id); err != nil {
		if err == sql.ErrNoRows {
			return id, false, nil
		}
		return id, false, errors.New("querying CDN ID from name: " + err.Error())
	}
	return id, true, nil
}
