// Package cachegroupparameter is deprecated and will be removed with API v1-3.
package cachegroupparameter

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

	"github.com/apache/trafficcontrol/v8/lib/go-log"
	"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/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/parameter"
	"github.com/apache/trafficcontrol/v8/traffic_ops/traffic_ops_golang/util/ims"

	"github.com/jmoiron/sqlx"
)

const (
	CacheGroupIDQueryParam      = "id"
	CacheGroupIDNamedQueryParam = "cachegroupID"
	ParameterIDQueryParam       = "parameterId"
)

// TOCacheGroupParameter is a type alias that is used to define CRUD functions on.
type TOCacheGroupParameter struct {
	api.APIInfoImpl `json:"-"`
	tc.CacheGroupParameterNullable
	CacheGroupID int `json:"-" db:"cachegroup_id"`
}

func (cgparam *TOCacheGroupParameter) ParamColumns() map[string]dbhelpers.WhereColumnInfo {
	return map[string]dbhelpers.WhereColumnInfo{
		CacheGroupIDQueryParam: dbhelpers.WhereColumnInfo{Column: "cgp.cachegroup", Checker: api.IsInt},
		ParameterIDQueryParam:  dbhelpers.WhereColumnInfo{Column: "p.id", Checker: api.IsInt},
	}
}

func (cgparam *TOCacheGroupParameter) GetType() string {
	return "cachegroup parameter"
}

func (cgparam *TOCacheGroupParameter) Read(h http.Header, useIMS bool) ([]interface{}, error, error, int, *time.Time) {
	var maxTime time.Time
	var runSecond bool
	queryParamsToQueryCols := cgparam.ParamColumns()
	parameters := cgparam.APIInfo().Params
	where, orderBy, pagination, queryValues, errs := dbhelpers.BuildWhereAndOrderByAndPagination(parameters, queryParamsToQueryCols)
	if len(errs) > 0 {
		return nil, util.JoinErrs(errs), nil, http.StatusBadRequest, nil
	}
	cgID, err := strconv.Atoi(parameters[CacheGroupIDQueryParam])
	if err != nil {
		return nil, errors.New("cache group id must be an integer"), nil, http.StatusBadRequest, nil
	}

	_, ok, err := dbhelpers.GetCacheGroupNameFromID(cgparam.ReqInfo.Tx.Tx, cgID)
	if err != nil {
		return nil, nil, err, http.StatusInternalServerError, nil
	} else if !ok {
		return nil, errors.New("cachegroup does not exist"), nil, http.StatusNotFound, nil
	}

	params := []interface{}{}
	if useIMS {
		runSecond, maxTime = ims.TryIfModifiedSinceQuery(cgparam.ReqInfo.Tx, h, queryValues, selectMaxLastUpdatedQuery(where))
		if !runSecond {
			log.Debugln("IMS HIT")
			return params, nil, nil, http.StatusNotModified, &maxTime
		}
		log.Debugln("IMS MISS")
	} else {
		log.Debugln("Non IMS request")
	}

	query := selectQuery() + where + orderBy + pagination
	rows, err := cgparam.ReqInfo.Tx.NamedQuery(query, queryValues)
	if err != nil {
		return nil, nil, errors.New("querying " + cgparam.GetType() + ": " + err.Error()), http.StatusInternalServerError, nil
	}
	defer rows.Close()

	for rows.Next() {
		var p tc.CacheGroupParameterNullable
		if err = rows.StructScan(&p); err != nil {
			return nil, nil, errors.New("scanning " + cgparam.GetType() + ": " + err.Error()), http.StatusInternalServerError, nil
		}
		if p.Secure != nil && *p.Secure && cgparam.ReqInfo.User.PrivLevel < auth.PrivLevelAdmin {
			p.Value = &parameter.HiddenField
		}
		params = append(params, p)
	}

	return params, nil, nil, http.StatusOK, &maxTime
}

func selectMaxLastUpdatedQuery(where string) string {
	return `SELECT max(t) from (
		SELECT max(p.last_updated) as t FROM parameter p
LEFT JOIN cachegroup_parameter cgp ON cgp.parameter = p.id ` + where +
		` UNION ALL
	select max(last_updated) as t from last_deleted l where l.table_name='parameter') as res`
}

func selectQuery() string {

	query := `SELECT
p.config_file,
p.id,
p.last_updated,
p.name,
p.value,
p.secure
FROM parameter p
LEFT JOIN cachegroup_parameter cgp ON cgp.parameter = p.id`
	return query
}

// GetKeyFieldsInfo implements the api.Identifier interface.
func (cgparam *TOCacheGroupParameter) GetKeyFieldsInfo() []api.KeyFieldInfo {
	return []api.KeyFieldInfo{
		{
			Field: CacheGroupIDNamedQueryParam,
			Func:  api.GetIntKey,
		},
		{
			Field: ParameterIDQueryParam,
			Func:  api.GetIntKey,
		},
	}
}

// SetKeys implements the api.Identifier interface and allows the
// delete handler to assign cachegroup and parameter ids.
func (cgparam *TOCacheGroupParameter) SetKeys(keys map[string]interface{}) {
	id, _ := keys[CacheGroupIDNamedQueryParam].(int)
	cgparam.CacheGroupID = id

	paramID, _ := keys[ParameterIDQueryParam].(int)
	cgparam.ID = &paramID
}

// DeleteQuery implements the api.GenericDeleter interface.
func (cgparam *TOCacheGroupParameter) DeleteQuery() string {
	return `DELETE FROM cachegroup_parameter
	WHERE cachegroup = :cachegroup_id AND parameter = :id`
}

// GetAuditName implements the api.Identifier interface.
func (cgparam *TOCacheGroupParameter) GetAuditName() string {
	if cgparam.ID != nil {
		return strconv.Itoa(cgparam.CacheGroupID) + "-" + strconv.Itoa(*cgparam.ID)
	}
	return "unknown"
}

// GetKeys implements the api.Identifier interface.
func (cgparam *TOCacheGroupParameter) GetKeys() (map[string]interface{}, bool) {
	if cgparam.ID == nil {
		return map[string]interface{}{ParameterIDQueryParam: 0}, false
	}
	return map[string]interface{}{
		CacheGroupIDNamedQueryParam: cgparam.CacheGroupID,
		ParameterIDQueryParam:       *cgparam.ID,
	}, true
}

// Delete implements the api.CRUDer interface.
func (cgparam *TOCacheGroupParameter) Delete() (error, error, int) {
	_, ok, err := dbhelpers.GetCacheGroupNameFromID(cgparam.ReqInfo.Tx.Tx, cgparam.CacheGroupID)
	if err != nil {
		return nil, err, http.StatusInternalServerError
	} else if !ok {
		return fmt.Errorf("cachegroup %v does not exist", cgparam.CacheGroupID), nil, http.StatusNotFound
	}

	_, ok, err = dbhelpers.GetParamNameByID(cgparam.ReqInfo.Tx.Tx, *cgparam.ID)
	if err != nil {
		return nil, err, http.StatusInternalServerError
	} else if !ok {
		return fmt.Errorf("parameter %v does not exist", *cgparam.ID), nil, http.StatusNotFound
	}

	// CheckIfCurrentUserCanModifyCachegroup
	userErr, sysErr, errCode := dbhelpers.CheckIfCurrentUserCanModifyCachegroup(cgparam.ReqInfo.Tx.Tx, cgparam.CacheGroupID, cgparam.ReqInfo.User.UserName)
	if userErr != nil || sysErr != nil {
		return userErr, sysErr, errCode
	}
	return api.GenericDelete(cgparam)
}

// ReadAllCacheGroupParameters reads all cachegroup parameter associations.
func ReadAllCacheGroupParameters(w http.ResponseWriter, r *http.Request) {
	inf, userErr, sysErr, errCode := api.NewInfo(r, nil, nil)
	if userErr != nil || sysErr != nil {
		api.HandleDeprecatedErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr, nil)
		return
	}
	defer inf.Close()
	output, err := GetAllCacheGroupParameters(inf.Tx, inf.Params)
	if err != nil {
		api.HandleDeprecatedErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, errors.New("querying cachegroupparameters with error: "+err.Error()), nil)
		return
	}
	api.WriteAlertsObj(w, r, http.StatusOK, api.CreateDeprecationAlerts(nil), output)
}

// GetAllCacheGroupParameters gets all cachegroup associations from the database and returns as slice.
func GetAllCacheGroupParameters(tx *sqlx.Tx, parameters map[string]string) (tc.CacheGroupParametersList, error) {
	if _, ok := parameters["orderby"]; !ok {
		parameters["orderby"] = "cachegroup"
	}

	// Query Parameters to Database Query column mappings
	// see the fields mapped in the SQL query
	queryParamsToQueryCols := map[string]dbhelpers.WhereColumnInfo{
		"cachegroup": dbhelpers.WhereColumnInfo{Column: "cgp.cachegroup", Checker: api.IsInt},
		"parameter":  dbhelpers.WhereColumnInfo{Column: "cgp.parameter", Checker: api.IsInt},
	}

	where, orderBy, pagination, queryValues, errs := dbhelpers.BuildWhereAndOrderByAndPagination(parameters, queryParamsToQueryCols)
	if len(errs) > 0 {
		return tc.CacheGroupParametersList{}, util.JoinErrs(errs)
	}

	query := selectAllQuery() + where + orderBy + pagination
	rows, err := tx.NamedQuery(query, queryValues)
	if err != nil {
		return tc.CacheGroupParametersList{}, errors.New("querying cachegroupParameters: " + err.Error())
	}
	defer rows.Close()

	paramsList := tc.CacheGroupParametersList{}
	params := []tc.CacheGroupParametersResponseNullable{}
	for rows.Next() {
		var p tc.CacheGroupParametersNullable
		if err = rows.Scan(&p.CacheGroup, &p.Parameter, &p.LastUpdated, &p.CacheGroupName); err != nil {
			return tc.CacheGroupParametersList{}, errors.New("scanning cachegroupParameters: " + err.Error())
		}
		params = append(params, tc.FormatForResponse(p))
	}
	paramsList.CacheGroupParameters = params
	return paramsList, nil
}

// AddCacheGroupParameters performs a Create for cachegroup parameter associations.
// AddCacheGroupParameters accepts data as a single association or an array of multiple.
func AddCacheGroupParameters(w http.ResponseWriter, r *http.Request) {
	inf, userErr, sysErr, errCode := api.NewInfo(r, nil, nil)
	if userErr != nil || sysErr != nil {
		api.HandleDeprecatedErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr, nil)
		return
	}
	defer inf.Close()
	data, err := ioutil.ReadAll(r.Body)
	if err != nil {
		api.HandleDeprecatedErr(w, r, inf.Tx.Tx, http.StatusBadRequest, errors.New("reading request body: "+err.Error()), nil, nil)
		return
	}

	buf := ioutil.NopCloser(bytes.NewReader(data))

	var paramsInt interface{}

	decoder := json.NewDecoder(buf)
	err = decoder.Decode(&paramsInt)
	if err != nil {
		api.HandleDeprecatedErr(w, r, inf.Tx.Tx, http.StatusBadRequest, errors.New("parsing json: "+err.Error()), nil, nil)
		return
	}

	var params []tc.CacheGroupParametersNullable
	_, ok := paramsInt.([]interface{})
	var parseErr error = nil
	if !ok {
		var singleParam tc.CacheGroupParametersNullable
		parseErr = json.Unmarshal(data, &singleParam)
		if singleParam.CacheGroup == nil || singleParam.Parameter == nil {
			api.HandleDeprecatedErr(w, r, inf.Tx.Tx, http.StatusBadRequest, errors.New("invalid cachegroup parameter."), nil, nil)
			return
		}
		params = append(params, singleParam)
	} else {
		parseErr = json.Unmarshal(data, &params)
	}
	if parseErr != nil {
		api.HandleDeprecatedErr(w, r, inf.Tx.Tx, http.StatusBadRequest, errors.New("parsing cachegroup parameter: "+parseErr.Error()), nil, nil)
		return
	}
	cachegroups := []int{}
	for _, p := range params {
		ppExists, err := dbhelpers.CachegroupParameterAssociationExists(*p.Parameter, *p.CacheGroup, inf.Tx.Tx)
		if err != nil {
			api.HandleDeprecatedErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, err, nil)
			return
		}
		if ppExists {
			api.HandleDeprecatedErr(w, r, inf.Tx.Tx, http.StatusBadRequest, errors.New("parameter: "+strconv.Itoa(*p.Parameter)+" already associated with cachegroup: "+strconv.Itoa(*p.CacheGroup)+"."), nil, nil)
			return
		}
		cachegroups = append(cachegroups, *p.CacheGroup)
	}
	userErr, sysErr, errCode = dbhelpers.CheckIfCurrentUserCanModifyCachegroups(inf.Tx.Tx, cachegroups, inf.User.UserName)
	if userErr != nil || sysErr != nil {
		api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr)
		return
	}
	values := []string{}
	for _, param := range params {
		values = append(values, "("+strconv.Itoa(*param.CacheGroup)+", "+strconv.Itoa(*param.Parameter)+")")
	}

	insQuery := strings.Join(values, ", ")
	_, err = inf.Tx.Tx.Query(insertQuery() + insQuery)

	if err != nil {
		userErr, sysErr, code := api.ParseDBError(err)
		api.HandleDeprecatedErr(w, r, inf.Tx.Tx, code, userErr, sysErr, nil)
		return
	}

	alerts := tc.CreateAlerts(tc.SuccessLevel, "Cachegroup parameter associations were created.")
	alerts.AddAlerts(api.CreateDeprecationAlerts(nil))
	api.WriteAlertsObj(w, r, http.StatusOK, alerts, params)
}

func selectAllQuery() string {
	return `SELECT cgp.cachegroup, cgp.parameter, cgp.last_updated, cg.name 
				FROM cachegroup_parameter AS cgp 
				JOIN cachegroup AS cg ON cg.id = cachegroup`
}

func insertQuery() string {
	return `INSERT INTO cachegroup_parameter 
		(cachegroup, 
		parameter) 
		VALUES `
}
