package steering

/*
 * 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 (
	"database/sql"
	"errors"
	"net/http"
	"sort"

	"github.com/apache/trafficcontrol/v8/lib/go-tc"
	"github.com/apache/trafficcontrol/v8/lib/go-util"
	"github.com/apache/trafficcontrol/v8/traffic_ops/traffic_ops_golang/api"

	"github.com/lib/pq"
)

func Get(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()

	steering, err := findSteering(inf.Tx.Tx)
	if err != nil {
		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, errors.New("steering.Get finding: "+err.Error()))
		return
	}
	api.WriteResp(w, r, steering)
}

func findSteering(tx *sql.Tx) ([]tc.Steering, error) {
	steeringData, err := getSteeringData(tx)
	if err != nil {
		return nil, err
	}
	targetIDs := steeringDataTargetIDs(steeringData)
	steeringFilters, err := getSteeringFilters(tx, targetIDs)
	if err != nil {
		return nil, err
	}
	primaryOriginCoords, err := getPrimaryOriginCoords(tx, targetIDs)
	if err != nil {
		return nil, err
	}

	steerings := map[tc.DeliveryServiceName]tc.Steering{}

	for _, data := range steeringData {
		if _, ok := steerings[data.DeliveryService]; !ok {
			steerings[data.DeliveryService] = tc.Steering{
				DeliveryService: data.DeliveryService,
				ClientSteering:  data.DSType == tc.DSTypeClientSteering,
				Filters:         []tc.SteeringFilter{},         // Initialize, so JSON produces `[]` not `null` if there are no filters.
				Targets:         []tc.SteeringSteeringTarget{}, // Initialize, so JSON produces `[]` not `null` if there are no targets.
			}
		}
		steering := steerings[data.DeliveryService]

		if filters, ok := steeringFilters[data.TargetID]; ok {
			steering.Filters = append(steering.Filters, filters...)
		}

		target := tc.SteeringSteeringTarget{DeliveryService: data.TargetName}
		switch data.Type {
		case tc.SteeringTypeOrder:
			target.Order = int32(data.Value)
		case tc.SteeringTypeWeight:
			target.Weight = int32(data.Value)
		case tc.SteeringTypeGeoOrder:
			target.GeoOrder = util.IntPtr(data.Value)
			target.Latitude = util.FloatPtr(primaryOriginCoords[data.TargetID].Lat)
			target.Longitude = util.FloatPtr(primaryOriginCoords[data.TargetID].Lon)
		case tc.SteeringTypeGeoWeight:
			target.Weight = int32(data.Value)
			target.GeoOrder = util.IntPtr(0)
			target.Latitude = util.FloatPtr(primaryOriginCoords[data.TargetID].Lat)
			target.Longitude = util.FloatPtr(primaryOriginCoords[data.TargetID].Lon)
		}
		steering.Targets = append(steering.Targets, target)
		steerings[data.DeliveryService] = steering
	}

	arr := []tc.Steering{}
	for _, steering := range steerings {
		arr = append(arr, steering)
	}

	sort.Slice(arr, func(i, j int) bool {
		return arr[i].DeliveryService < arr[j].DeliveryService
	})

	return arr, nil
}

type SteeringData struct {
	DeliveryService tc.DeliveryServiceName
	SteeringID      int
	TargetName      tc.DeliveryServiceName
	TargetID        int
	Value           int
	Type            tc.SteeringType
	DSType          tc.DSType
}

func steeringDataTargetIDs(data []SteeringData) []int {
	ids := []int{}
	for _, d := range data {
		ids = append(ids, d.TargetID)
	}
	return ids
}

func getSteeringData(tx *sql.Tx) ([]SteeringData, error) {
	qry := `
SELECT
  ds.xml_id as steering_xml_id,
  ds.id as steering_id,
  t.xml_id as target_xml_id,
  t.id as target_id,
  st.value,
  tp.name as steering_type,
  dt.name as ds_type
FROM
  steering_target st
  JOIN deliveryservice ds on ds.id = st.deliveryservice
  JOIN deliveryservice t on t.id = st.target
  JOIN type tp on tp.id = st.type
  JOIN type dt on dt.id = ds.type
ORDER BY
  steering_xml_id,
  target_xml_id
`
	rows, err := tx.Query(qry)
	if err != nil {
		return nil, errors.New("querying steering: " + err.Error())
	}
	defer rows.Close()
	data := []SteeringData{}
	for rows.Next() {
		sd := SteeringData{}
		if err := rows.Scan(&sd.DeliveryService, &sd.SteeringID, &sd.TargetName, &sd.TargetID, &sd.Value, &sd.Type, &sd.DSType); err != nil {
			return nil, errors.New("get steering data scanning: " + err.Error())
		}
		data = append(data, sd)
	}
	return data, nil
}

// getSteeringFilters takes a slice of ds ids, and returns a map of delivery service ids to patterns and delivery service names.
func getSteeringFilters(tx *sql.Tx, dsIDs []int) (map[int][]tc.SteeringFilter, error) {
	qry := `
SELECT
  ds.id,
  ds.xml_id,
  r.pattern
FROM
  deliveryservice ds
  JOIN deliveryservice_regex dsr ON dsr.deliveryservice = ds.id
  JOIN regex r ON dsr.regex = r.id
  JOIN type t ON r.type = t.id
WHERE
  ds.id = ANY($1)
  AND t.name = $2
ORDER BY
  r.pattern,
  ds.type,
  dsr.set_number
`
	rows, err := tx.Query(qry, pq.Array(dsIDs), tc.DSMatchTypeSteeringRegex)
	if err != nil {
		return nil, errors.New("querying steering regexes: " + err.Error())
	}
	defer rows.Close()
	filters := map[int][]tc.SteeringFilter{}
	for rows.Next() {
		dsID := 0
		f := tc.SteeringFilter{}
		if err := rows.Scan(&dsID, &f.DeliveryService, &f.Pattern); err != nil {
			return nil, errors.New("scanning steering filters: " + err.Error())
		}
		filters[dsID] = append(filters[dsID], f)
	}
	return filters, nil
}

type Coord struct {
	Lat float64
	Lon float64
}

// getPrimaryOriginCoords takes a slice of ds ids, and returns a map of delivery service ids to their primary origin coordinates.
func getPrimaryOriginCoords(tx *sql.Tx, dsIDs []int) (map[int]Coord, error) {
	qry := `
SELECT
  o.deliveryservice,
  c.latitude,
  c.longitude
FROM
  origin o
  JOIN coordinate c ON c.id = o.coordinate
WHERE
  o.deliveryservice = ANY($1)
  AND o.is_primary
`
	rows, err := tx.Query(qry, pq.Array(dsIDs))
	if err != nil {
		return nil, errors.New("querying steering primary origin coords: " + err.Error())
	}
	defer rows.Close()
	coords := map[int]Coord{}
	for rows.Next() {
		dsID := 0
		c := Coord{}
		if err := rows.Scan(&dsID, &c.Lat, &c.Lon); err != nil {
			return nil, errors.New("scanning steering primary origin coords: " + err.Error())
		}
		coords[dsID] = c
	}
	return coords, nil
}
