package parameter

/*
 * 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"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"net/http"
	"strconv"
	"time"

	"github.com/apache/trafficcontrol/v8/lib/go-atscfg"
	"github.com/apache/trafficcontrol/v8/lib/go-log"
	"github.com/apache/trafficcontrol/v8/lib/go-tc"
	"github.com/apache/trafficcontrol/v8/lib/go-tc/tovalidate"
	"github.com/apache/trafficcontrol/v8/lib/go-util"
	"github.com/apache/trafficcontrol/v8/traffic_ops/traffic_ops_golang/api"
	"github.com/apache/trafficcontrol/v8/traffic_ops/traffic_ops_golang/auth"
	"github.com/apache/trafficcontrol/v8/traffic_ops/traffic_ops_golang/dbhelpers"
	"github.com/apache/trafficcontrol/v8/traffic_ops/traffic_ops_golang/util/ims"

	validation "github.com/go-ozzo/ozzo-validation"
)

const (
	NameQueryParam       = "name"
	SecureQueryParam     = "secure"
	ConfigFileQueryParam = "configFile"
	IDQueryParam         = "id"
	ValueQueryParam      = "value"
	CommentQueryParam    = "comment"
)

var (
	HiddenField = "********"
)

// we need a type alias to define functions on
type TOParameter struct {
	api.APIInfoImpl `json:"-"`
	tc.ParameterNullable
}

func (v *TOParameter) GetLastUpdated() (*time.Time, bool, error) {
	return api.GetLastUpdated(v.APIInfo().Tx, *v.ID, "parameter")
}

// AllowMultipleCreates indicates whether an array can be POSTed using the shared Create handler
func (v *TOParameter) AllowMultipleCreates() bool    { return true }
func (v *TOParameter) SetLastUpdated(t tc.TimeNoMod) { v.LastUpdated = &t }
func (v *TOParameter) InsertQuery() string           { return insertQuery() }
func (v *TOParameter) SelectMaxLastUpdatedQuery(where, orderBy, pagination, tableName string) string {
	return `SELECT max(t) from (
		SELECT max(p.last_updated) as t FROM parameter p
LEFT JOIN profile_parameter pp ON p.id = pp.parameter
LEFT JOIN profile pr ON pp.profile = pr.id ` + where + orderBy + pagination +
		` UNION ALL
	select max(last_updated) as t from last_deleted l where l.table_name='` + tableName + `') as res`
}

func (v *TOParameter) NewReadObj() interface{} { return &tc.ParameterNullable{} }
func (v *TOParameter) SelectQuery() string     { return selectQuery() }
func (v *TOParameter) ParamColumns() map[string]dbhelpers.WhereColumnInfo {
	return map[string]dbhelpers.WhereColumnInfo{
		ConfigFileQueryParam: {Column: "p.config_file"},
		IDQueryParam:         {Column: "p.id", Checker: api.IsInt},
		NameQueryParam:       {Column: "p.name"},
		SecureQueryParam:     {Column: "p.secure", Checker: api.IsBool},
		ValueQueryParam:      {Column: "p.value"},
		CommentQueryParam:    {Column: "p.comment"},
	}
}
func (v *TOParameter) UpdateQuery() string { return updateQuery() }
func (v *TOParameter) DeleteQuery() string { return deleteQuery() }

func (param TOParameter) GetKeyFieldsInfo() []api.KeyFieldInfo {
	return []api.KeyFieldInfo{{Field: IDQueryParam, Func: api.GetIntKey}}
}

// Implementation of the Identifier, Validator interface functions
func (param TOParameter) GetKeys() (map[string]interface{}, bool) {
	if param.ID == nil {
		return map[string]interface{}{IDQueryParam: 0}, false
	}
	return map[string]interface{}{IDQueryParam: *param.ID}, true
}

func (param *TOParameter) SetKeys(keys map[string]interface{}) {
	i, _ := keys[IDQueryParam].(int) //this utilizes the non panicking type assertion, if the thrown away ok variable is false i will be the zero of the type, 0 here.
	param.ID = &i
}

func (param *TOParameter) GetAuditName() string {
	if param.Name != nil {
		return *param.Name
	}
	if param.ID != nil {
		return strconv.Itoa(*param.ID)
	}
	return "unknown"
}

func (param *TOParameter) GetType() string {
	return "param"
}

// Validate fulfills the api.Validator interface
func (param TOParameter) Validate() (error, error) {
	// Test
	// - Secure Flag is always set to either 1/0
	// - Admin rights only
	// - Do not allow duplicate parameters by name+config_file+value
	// - Client can send NOT NULL constraint on 'value' so removed it's validation as .Required
	errs := validation.Errors{
		NameQueryParam:       validation.Validate(param.Name, validation.Required),
		ConfigFileQueryParam: validation.Validate(param.ConfigFile, validation.Required),
	}
	if *param.ConfigFile == atscfg.ParentConfigFileName && *param.Name == atscfg.ParentConfigCacheParamWeight {
		errs[atscfg.ParentConfigFileName+" "+atscfg.ParentConfigCacheParamWeight] = validation.Validate(*param.Value, tovalidate.StringIsValidFloat())
	}

	return util.JoinErrs(tovalidate.ToErrors(errs)), nil
}

func (pa *TOParameter) Create() (error, error, int) {
	if pa.Value == nil {
		pa.Value = util.StrPtr("")
	}
	return api.GenericCreate(pa)
}

func (param *TOParameter) Read(h http.Header, useIMS bool) ([]interface{}, error, error, int, *time.Time) {
	var maxTime time.Time
	var runSecond bool
	code := http.StatusOK
	queryParamsToQueryCols := param.ParamColumns()
	where, orderBy, pagination, queryValues, errs := dbhelpers.BuildWhereAndOrderByAndPagination(param.APIInfo().Params, queryParamsToQueryCols)
	if len(errs) > 0 {
		return nil, util.JoinErrs(errs), nil, http.StatusBadRequest, nil
	}
	if useIMS {
		runSecond, maxTime = ims.TryIfModifiedSinceQuery(param.APIInfo().Tx, h, queryValues, param.SelectMaxLastUpdatedQuery(where, orderBy, pagination, "parameter"))
		if !runSecond {
			log.Debugln("IMS HIT")
			return []interface{}{}, nil, nil, http.StatusNotModified, &maxTime
		}
		log.Debugln("IMS MISS")
	} else {
		log.Debugln("Non IMS request")
	}
	query := selectQuery() + where + ParametersGroupBy() + orderBy + pagination
	rows, err := param.ReqInfo.Tx.NamedQuery(query, queryValues)
	if err != nil {
		return nil, nil, errors.New("querying " + param.GetType() + ": " + err.Error()), http.StatusInternalServerError, nil
	}
	defer rows.Close()

	params := []interface{}{}
	for rows.Next() {
		var p tc.ParameterNullable
		if err = rows.StructScan(&p); err != nil {
			return nil, nil, errors.New("scanning " + param.GetType() + ": " + err.Error()), http.StatusInternalServerError, nil
		}
		if p.Secure != nil && *p.Secure {
			if param.ReqInfo.Version.Major >= 5 {
				if !param.ReqInfo.User.Can("PARAMETER-SECURE:READ") {
					p.Value = &HiddenField
				}
			} else if param.ReqInfo.Version.Major == 4 {
				if param.ReqInfo.Config.RoleBasedPermissions {
					if !param.ReqInfo.User.Can("PARAMETER-SECURE:READ") {
						p.Value = &HiddenField
					}
				} else if param.ReqInfo.User.PrivLevel < auth.PrivLevelAdmin {
					p.Value = &HiddenField
				}
			} else if param.ReqInfo.User.PrivLevel < auth.PrivLevelAdmin {
				p.Value = &HiddenField
			}
		}
		params = append(params, p)
	}

	return params, nil, nil, code, &maxTime
}

func (pa *TOParameter) Update(h http.Header) (error, error, int) {
	if pa.Value == nil {
		pa.Value = util.StrPtr("")
	}
	return api.GenericUpdate(h, pa)
}

func (pa *TOParameter) Delete() (error, error, int) { return api.GenericDelete(pa) }

func insertQuery() string {
	query := `INSERT INTO parameter (
name,
config_file,
value,
secure,
comment) VALUES (
:name,
:config_file,
:value,
:secure,
:comment) RETURNING id,last_updated`
	return query
}

func selectQuery() string {

	query := `SELECT
p.config_file,
p.id,
p.last_updated,
p.name,
p.value,
p.secure,
p.comment,
COALESCE(array_to_json(array_agg(pr.name) FILTER (WHERE pr.name IS NOT NULL)), '[]') AS profiles
FROM parameter p
LEFT JOIN profile_parameter pp ON p.id = pp.parameter
LEFT JOIN profile pr ON pp.profile = pr.id`
	return query
}

func updateQuery() string {
	query := `UPDATE
parameter SET
config_file=:config_file,
id=:id,
name=:name,
value=:value,
secure=:secure,
comment=:comment
WHERE id=:id RETURNING last_updated`
	return query
}

// ParametersGroupBy ...
func ParametersGroupBy() string {
	groupBy := ` GROUP BY p.config_file, p.id, p.last_updated, p.name, p.value, p.secure, p.comment`
	return groupBy
}

func deleteQuery() string {
	query := `DELETE FROM parameter
WHERE id=:id`
	return query
}

func GetParameters(w http.ResponseWriter, r *http.Request) {
	var runSecond bool
	var maxTime time.Time
	inf, userErr, sysErr, errCode := api.NewInfo(r, nil, nil)
	tx := inf.Tx
	if userErr != nil || sysErr != nil {
		api.HandleErr(w, r, tx.Tx, errCode, userErr, sysErr)
		return
	}
	defer inf.Close()

	// Query Parameters to Database Query column mappings
	queryParamsToQueryCols := map[string]dbhelpers.WhereColumnInfo{
		ConfigFileQueryParam: {Column: "p.config_file"},
		IDQueryParam:         {Column: "p.id", Checker: api.IsInt},
		NameQueryParam:       {Column: "p.name"},
		SecureQueryParam:     {Column: "p.secure", Checker: api.IsBool},
		ValueQueryParam:      {Column: "p.value"},
		CommentQueryParam:    {Column: "p.comment"},
	}
	if _, ok := inf.Params["orderby"]; !ok {
		inf.Params["orderby"] = "name"
	}
	where, orderBy, pagination, queryValues, errs := dbhelpers.BuildWhereAndOrderByAndPagination(inf.Params, queryParamsToQueryCols)
	if len(errs) > 0 {
		api.HandleErr(w, r, tx.Tx, http.StatusBadRequest, util.JoinErrs(errs), nil)
		return
	}

	if inf.Config.UseIMS {
		runSecond, maxTime = ims.TryIfModifiedSinceQuery(tx, r.Header, queryValues, selectMaxLastUpdatedQuery(where))
		if !runSecond {
			log.Debugln("IMS HIT")
			api.AddLastModifiedHdr(w, maxTime)
			w.WriteHeader(http.StatusNotModified)
			return
		}
		log.Debugln("IMS MISS")
	} else {
		log.Debugln("Non IMS request")
	}

	query := selectQuery() + where + ParametersGroupBy() + orderBy + pagination
	rows, err := tx.NamedQuery(query, queryValues)
	if err != nil {
		api.HandleErr(w, r, tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("Parameter read: error getting Parameter(s): %w", err))
		return
	}
	defer log.Close(rows, "unable to close DB connection")

	params := tc.ParameterNullableV5{}
	paramsList := []tc.ParameterNullableV5{}
	for rows.Next() {
		if err = rows.Scan(&params.ConfigFile, &params.ID, &params.LastUpdated, &params.Name, &params.Value, &params.Secure, &params.Comment, &params.Profiles); err != nil {
			api.HandleErr(w, r, tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("error getting parameter(s): %w", err))
			return
		}
		if params.Secure != nil && *params.Secure {
			if inf.Version.Major >= 4 &&
				inf.Config.RoleBasedPermissions &&
				!inf.User.Can("PARAMETER-SECURE:READ") {
				params.Value = &HiddenField
			} else if inf.User.PrivLevel < auth.PrivLevelAdmin {
				params.Value = &HiddenField
			}
		}

		paramsList = append(paramsList, params)
	}

	api.WriteResp(w, r, paramsList)
	return
}

func CreateParameter(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()
	tx := inf.Tx.Tx

	body, err := io.ReadAll(r.Body)
	if err != nil {
		api.HandleErr(w, r, tx, http.StatusBadRequest, errors.New("error reading request body"), nil)
		return
	}
	defer r.Body.Close()

	// Initial Unmarshal to validate request body
	var data interface{}
	err = json.Unmarshal(body, &data)
	if err != nil {
		api.HandleErr(w, r, tx, http.StatusBadRequest, errors.New("invalid request format"), nil)
		return
	}

	// This code block decides if the request body is a slice of parameters or a single object and unmarshal it.
	var params []tc.ParameterV5
	if err := json.Unmarshal(body, &params); err != nil {
		var param tc.ParameterV5
		if err := json.Unmarshal(body, &param); err != nil {
			api.HandleErr(w, r, tx, http.StatusBadRequest, errors.New("error unmarshalling single object"), nil)
			return
		}
		params = append(params, param)
	}
	// Validate all objects of the every parameter from the request slice
	for _, parameter := range params {
		readValErr := validateRequestParameter(parameter)
		if readValErr != nil {
			api.HandleErr(w, r, tx, http.StatusBadRequest, readValErr, nil)
			return
		}
	}

	// Create all parameters from the request slice
	var objParams []tc.ParameterV5
	var objParam tc.ParameterV5
	for _, parameter := range params {
		query := `
			INSERT INTO parameter (
			    name,
			    config_file,
			    value,
			    secure,
			    comment
			    ) VALUES (
			        $1, $2, $3, $4, $5
			    ) RETURNING id, name, config_file, value, last_updated, secure, comment 
		`
		err = tx.QueryRow(
			query,
			parameter.Name,
			parameter.ConfigFile,
			parameter.Value,
			parameter.Secure,
			parameter.Comment,
		).Scan(

			&objParam.ID,
			&objParam.Name,
			&objParam.ConfigFile,
			&objParam.Value,
			&objParam.LastUpdated,
			&objParam.Secure,
			&objParam.Comment,
		)

		if err != nil {
			if errors.Is(err, sql.ErrNoRows) {
				api.HandleErr(w, r, tx, http.StatusInternalServerError, fmt.Errorf("error: %w in parameter  with name: %s", err, parameter.Name), nil)
				return
			}
			usrErr, sysErr, code := api.ParseDBError(err)
			api.HandleErr(w, r, tx, code, usrErr, sysErr)
			return
		}

		objParams = append(objParams, objParam)
	}
	alerts := tc.CreateAlerts(tc.SuccessLevel, "All Requested Parameters were created.")
	api.WriteAlertsObj(w, r, http.StatusCreated, alerts, objParam)
	for _, param := range params {
		changeLogMsg := fmt.Sprintf("PARAMETER: %s, ID:%d, ACTION: Created parameter", param.Name, param.ID)
		api.CreateChangeLogRawTx(api.Created, changeLogMsg, inf.User, tx)
	}
	return
}

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

	tx := inf.Tx.Tx
	parameter, readValErr := readAndValidateJsonStruct(r)
	if readValErr != nil {
		api.HandleErr(w, r, tx, http.StatusBadRequest, readValErr, nil)
		return
	}

	requestedID := inf.Params["id"]
	intRequestId := inf.IntParams["id"]

	// check if the entity was already updated
	userErr, sysErr, errCode = api.CheckIfUnModified(r.Header, inf.Tx, intRequestId, "parameter")
	if userErr != nil || sysErr != nil {
		api.HandleErr(w, r, tx, errCode, userErr, sysErr)
		return
	}

	//update name and description of a phys_location
	query := `
	UPDATE parameter p
	SET
		config_file = $1,
		name = $2,
		value = $3,
		secure = $4,
		comment = $5
	WHERE
		p.id = $6
	RETURNING
		p.id,
		p.last_updated
`

	err := tx.QueryRow(
		query,
		parameter.ConfigFile,
		parameter.Name,
		parameter.Value,
		parameter.Secure,
		parameter.Comment,
		requestedID,
	).Scan(
		&parameter.ID,
		&parameter.LastUpdated,
	)
	if err != nil {
		if errors.Is(err, sql.ErrNoRows) {
			api.HandleErr(w, r, tx, http.StatusBadRequest, fmt.Errorf("parameter with ID: %v not found", parameter.ID), nil)
			return
		}
		usrErr, sysErr, code := api.ParseDBError(err)
		api.HandleErr(w, r, tx, code, usrErr, sysErr)
		return
	}
	alerts := tc.CreateAlerts(tc.SuccessLevel, "parameter was updated")
	api.WriteAlertsObj(w, r, http.StatusOK, alerts, parameter)
	changeLogMsg := fmt.Sprintf("PARAMETER: %s, ID:%d, ACTION: Updated parameter", parameter.Name, parameter.ID)
	api.CreateChangeLogRawTx(api.Updated, changeLogMsg, inf.User, tx)
	return
}

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

	id := inf.Params["id"]
	if id == "" {
		api.HandleErr(w, r, tx, http.StatusBadRequest, fmt.Errorf("couldn't delete Parameter. Invalid ID. Id Cannot be empty for Delete Operation"), nil)
		return
	}
	exists, err := dbhelpers.ParameterExists(tx, id)
	if err != nil {
		api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, err)
		return
	}
	if !exists {
		api.HandleErr(w, r, tx, http.StatusNotFound, fmt.Errorf("no Parameter exists by id: %s", id), nil)
		return
	}

	res, err := tx.Exec("DELETE FROM parameter AS p WHERE p.id=$1", id)
	if err != nil {
		api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, err)
		return
	}
	rowsAffected, err := res.RowsAffected()
	if err != nil {
		api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, fmt.Errorf("error determining rows affected for delete parameter: %w", err))
		return
	}
	if rowsAffected == 0 {
		api.HandleErr(w, r, tx, http.StatusInternalServerError, fmt.Errorf("no rows deleted for parameter"), nil)
		return
	}
	alerts := tc.CreateAlerts(tc.SuccessLevel, "parameter"+
		" was deleted.")
	api.WriteAlerts(w, r, http.StatusOK, alerts)
	changeLogMsg := fmt.Sprintf("ID:%s, ACTION: Deleted parameter", id)
	api.CreateChangeLogRawTx(api.Deleted, changeLogMsg, inf.User, tx)
	return
}

// selectMaxLastUpdatedQuery used for TryIfModifiedSinceQuery()
func selectMaxLastUpdatedQuery(where string) string {
	return `
SELECT max(t) from (
	SELECT max(p.last_updated) as t FROM parameter p
	LEFT JOIN profile_parameter pp ON p.id = pp.parameter
	LEFT JOIN profile pr ON pp.profile = pr.id ` + where +
		` UNION ALL
	select max(last_updated) as t from last_deleted l where l.table_name='parameter'
) as res`
}

func readAndValidateJsonStruct(r *http.Request) (tc.ParameterV5, error) {
	var parameter tc.ParameterV5
	if err := json.NewDecoder(r.Body).Decode(&parameter); err != nil {
		userErr := fmt.Errorf("error decoding POST request body into ParameterV5 struct %w", err)
		return parameter, userErr
	}

	// validate JSON body
	errs := make(map[string]error)

	errs[NameQueryParam] = validation.Validate(parameter.Name, validation.Required)
	errs[ConfigFileQueryParam] = validation.Validate(parameter.ConfigFile, validation.Required)

	if parameter.ConfigFile == atscfg.ParentConfigFileName && parameter.Name == atscfg.ParentConfigCacheParamWeight {
		key := atscfg.ParentConfigFileName + " " + atscfg.ParentConfigCacheParamWeight
		errs[key] = validation.Validate(parameter.Value, tovalidate.StringIsValidFloat())
	}

	if len(errs) > 0 {
		var errorSlice []error
		for _, err := range errs {
			errorSlice = append(errorSlice, err)
		}
		userErr := util.JoinErrs(errorSlice)
		return parameter, userErr
	}
	return parameter, nil
}

// Unlike usual readAndValidateJsonStruct function, this function does not decode the JSON data
// but only validate the JSON objects
func validateRequestParameter(parameter tc.ParameterV5) error {
	errs := make(map[string]error)

	errs[NameQueryParam] = validation.Validate(parameter.Name, validation.Required)
	errs[ConfigFileQueryParam] = validation.Validate(parameter.ConfigFile, validation.Required)

	if parameter.ConfigFile == atscfg.ParentConfigFileName && parameter.Name == atscfg.ParentConfigCacheParamWeight {
		key := atscfg.ParentConfigFileName + " " + atscfg.ParentConfigCacheParamWeight
		errs[key] = validation.Validate(parameter.Value, tovalidate.StringIsValidFloat())
	}

	if len(errs) > 0 {
		var errorSlice []error
		for _, err := range errs {
			errorSlice = append(errorSlice, err)
		}
		userErr := util.JoinErrs(errorSlice)
		return userErr
	}
	return nil
}
