// Copyright (c) 2016 Intel Corporation
//
// Licensed 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.

package main

import (
	"encoding/json"
	"errors"
	"fmt"
	"io/ioutil"
	"net/http"
	"sort"
	"strconv"
	"time"

	"github.com/ciao-project/ciao/ciao-controller/types"
	"github.com/ciao-project/ciao/payloads"
	"github.com/ciao-project/ciao/ssntp"
	"github.com/golang/glog"
	"github.com/gorilla/mux"
)

// HTTPErrorData represents the HTTP response body for
// a compute API request error.
type HTTPErrorData struct {
	Code    int    `json:"code"`
	Name    string `json:"name"`
	Message string `json:"message"`
}

// HTTPReturnErrorCode represents the unmarshalled version for Return codes
// when a API call is made and you need to return explicit data of
// the call as OpenStack format
// http://developer.openstack.org/api-guide/compute/faults.html
type HTTPReturnErrorCode struct {
	Error HTTPErrorData `json:"error"`
}

// APIResponse contains the http status and any response struct to be marshalled.
type APIResponse struct {
	status   int
	response interface{}
}

func errorResponse(err error) APIResponse {
	switch err {
	case types.ErrQuota:
		return APIResponse{http.StatusForbidden, nil}
	case types.ErrTenantNotFound,
		types.ErrInstanceNotFound:
		return APIResponse{http.StatusNotFound, nil}
	default:
		return APIResponse{http.StatusInternalServerError, nil}
	}
}

type pagerFilterType uint8

const (
	none pagerFilterType = 0
)

func pagerQueryParse(r *http.Request) (int, int, string) {
	values := r.URL.Query()
	limit := 0
	offset := 0
	marker := ""
	if values["limit"] != nil {
		l, err := strconv.ParseInt(values["limit"][0], 10, 32)
		if err != nil {
			limit = 0
		} else {
			limit = (int)(l)
		}
	}

	if values["marker"] != nil {
		marker = values["marker"][0]
	} else if values["offset"] != nil {
		o, err := strconv.ParseInt(values["offset"][0], 10, 32)
		if err != nil {
			offset = 0
		} else {
			offset = (int)(o)
		}
	}

	return limit, offset, marker
}

type nodePager struct {
	ctl   *controller
	nodes []types.CiaoNode
}

func (pager *nodePager) getNodes(filterType pagerFilterType, filter string, nodes []types.CiaoNode, limit int, offset int) (types.CiaoNodes, error) {
	computeNodes := types.NewCiaoNodes()

	pageLength := 0

	glog.V(2).Infof("Get nodes limit [%d] offset [%d]", limit, offset)

	if nodes == nil || offset >= len(nodes) {
		return computeNodes, nil
	}

	for _, node := range nodes[offset:] {
		computeNodes.Nodes = append(computeNodes.Nodes, node)

		pageLength++
		if limit > 0 && pageLength >= limit {
			break
		}
	}

	return computeNodes, nil
}

func (pager *nodePager) nextPage(filterType pagerFilterType, filter string, r *http.Request) (types.CiaoNodes, error) {
	limit, offset, lastSeen := pagerQueryParse(r)

	if lastSeen == "" {
		if limit != 0 {
			return pager.getNodes(filterType, filter, pager.nodes,
				limit, offset)
		}

		return pager.getNodes(filterType, filter, pager.nodes, 0,
			offset)
	}

	for i, node := range pager.nodes {
		if node.ID == lastSeen {
			if i >= len(pager.nodes)-1 {
				return pager.getNodes(filterType, filter, nil,
					limit, 0)
			}

			return pager.getNodes(filterType, filter,
				pager.nodes[i+1:], limit, 0)
		}
	}

	return types.CiaoNodes{}, fmt.Errorf("Item %s not found", lastSeen)
}

type nodeServerPager struct {
	ctl       *controller
	instances []types.CiaoServerStats
}

func (pager *nodeServerPager) getNodeServers(filterType pagerFilterType, filter string, instances []types.CiaoServerStats,
	limit int, offset int) (types.CiaoServersStats, error) {
	servers := types.NewCiaoServersStats()

	servers.TotalServers = len(instances)
	pageLength := 0

	glog.V(2).Infof("Get nodes limit [%d] offset [%d]", limit, offset)

	if instances == nil || offset >= len(instances) {
		return servers, nil
	}

	for _, instance := range instances[offset:] {
		servers.Servers = append(servers.Servers, instance)

		pageLength++
		if limit > 0 && pageLength >= limit {
			break
		}
	}

	return servers, nil
}

func (pager *nodeServerPager) nextPage(filterType pagerFilterType, filter string, r *http.Request) (types.CiaoServersStats, error) {
	limit, offset, lastSeen := pagerQueryParse(r)

	glog.V(2).Infof("Next page marker [%s] limit [%d] offset [%d]",
		lastSeen, limit, offset)

	if lastSeen == "" {
		if limit != 0 {
			return pager.getNodeServers(filterType, filter,
				pager.instances, limit, offset)
		}

		return pager.getNodeServers(filterType, filter,
			pager.instances, 0, offset)
	}

	for i, instance := range pager.instances {
		if instance.ID == lastSeen {
			if i >= len(pager.instances)-1 {
				return pager.getNodeServers(filterType, filter,
					nil, limit, 0)
			}

			return pager.getNodeServers(filterType, filter,
				pager.instances[i+1:], limit, 0)
		}
	}

	return types.CiaoServersStats{}, fmt.Errorf("Item %s not found", lastSeen)
}

func findQuota(qds []types.QuotaDetails, name string) *types.QuotaDetails {
	for i := range qds {
		if qds[i].Name == name {
			return &qds[i]
		}
	}
	return nil
}

func getResources(c *controller, w http.ResponseWriter, r *http.Request) (APIResponse, error) {
	var tenantResource types.CiaoTenantResources

	vars := mux.Vars(r)
	tenant := vars["tenant"]

	t, err := c.ds.GetTenant(tenant)
	if err != nil || t == nil {
		return errorResponse(types.ErrTenantNotFound), types.ErrTenantNotFound
	}

	tenantResource.ID = t.ID

	qds := c.qs.DumpQuotas(t.ID)

	qd := findQuota(qds, "tenant-instances-quota")
	if qd != nil {
		tenantResource.InstanceLimit = qd.Value
		tenantResource.InstanceUsage = qd.Usage
	}
	qd = findQuota(qds, "tenant-vcpu-quota")
	if qd != nil {
		tenantResource.VCPULimit = qd.Value
		tenantResource.VCPUUsage = qd.Usage
	}
	qd = findQuota(qds, "tenant-mem-quota")
	if qd != nil {
		tenantResource.MemLimit = qd.Value
		tenantResource.MemUsage = qd.Usage
	}
	qd = findQuota(qds, "tenant-storage-quota")
	if qd != nil {
		tenantResource.DiskLimit = qd.Value
		tenantResource.DiskUsage = qd.Usage
	}

	return APIResponse{http.StatusOK, tenantResource}, nil
}

func tenantQueryParse(r *http.Request) (time.Time, time.Time, error) {
	values := r.URL.Query()
	var startTime, endTime time.Time

	if values["start_date"] == nil || values["end_date"] == nil {
		return startTime, endTime, fmt.Errorf("Missing date")
	}

	startTime, err := time.Parse(time.RFC3339, values["start_date"][0])
	if err != nil {
		return startTime, endTime, err
	}

	endTime, err = time.Parse(time.RFC3339, values["end_date"][0])
	if err != nil {
		return startTime, endTime, err
	}

	return startTime, endTime, nil
}

func getUsage(c *controller, w http.ResponseWriter, r *http.Request) (APIResponse, error) {
	vars := mux.Vars(r)
	tenant := vars["tenant"]
	var usage types.CiaoUsageHistory

	start, end, err := tenantQueryParse(r)
	if err != nil {
		return errorResponse(err), err
	}

	glog.V(2).Infof("Start %v\n", start)
	glog.V(2).Infof("End %v\n", end)

	usage.Usages, err = c.ds.GetTenantUsage(tenant, start, end)
	if err != nil {
		return errorResponse(err), err
	}

	return APIResponse{http.StatusOK, usage}, nil
}

type instanceAction func(string) error

func serversAction(c *controller, w http.ResponseWriter, r *http.Request) (APIResponse, error) {
	vars := mux.Vars(r)
	tenant := vars["tenant"]
	var servers types.CiaoServersAction
	var actionFunc instanceAction
	var statusFilter string

	body, err := ioutil.ReadAll(r.Body)
	if err != nil {
		return errorResponse(err), err
	}

	err = json.Unmarshal(body, &servers)
	if err != nil {
		return errorResponse(err), err
	}

	if servers.Action == "os-start" {
		actionFunc = c.restartInstance
		statusFilter = payloads.Exited
	} else if servers.Action == "os-stop" {
		actionFunc = c.stopInstance
		statusFilter = payloads.Running
	} else if servers.Action == "os-delete" {
		actionFunc = c.deleteInstance
		statusFilter = ""
	} else {
		return APIResponse{http.StatusServiceUnavailable, nil},
			errors.New("Unsupported action")
	}

	if len(servers.ServerIDs) > 0 {
		for _, instanceID := range servers.ServerIDs {
			// make sure the instance belongs to the tenant
			_, err := c.ds.GetTenantInstance(tenant, instanceID)
			if err != nil {
				return errorResponse(err), err
			}

			err = actionFunc(instanceID)
			if err != nil {
				return errorResponse(err), err
			}
		}
	} else {
		/* We want to act on all relevant instances */
		instances, err := c.ds.GetAllInstancesFromTenant(tenant)
		if err != nil {
			return errorResponse(err), err
		}

		for _, instance := range instances {
			if statusFilter != "" &&
				instance.State != statusFilter {
				continue
			}

			err = actionFunc(instance.ID)
			if err != nil {
				return errorResponse(err), err
			}
		}
	}

	return APIResponse{http.StatusAccepted, nil}, nil
}

func trimComputeNodes(c *controller, nodeList types.CiaoNodes, targetRole ssntp.Role) (types.CiaoNodes, error) {
	var trimmedNodes types.CiaoNodes

	for _, node := range nodeList.Nodes {
		n, err := c.ds.GetNode(node.ID)
		if err != nil {
			continue
		}

		if n.NodeRole.HasRole(targetRole) {
			trimmedNodes.Nodes = append(trimmedNodes.Nodes, node)
		}
	}

	sort.Sort(types.SortedNodesByID(trimmedNodes.Nodes))

	return trimmedNodes, nil
}

func listSubsetOfNodes(c *controller, w http.ResponseWriter, r *http.Request, targetRole ssntp.Role) (APIResponse, error) {
	allNodes := c.ds.GetNodeLastStats()

	var subsetOfNodes types.CiaoNodes
	if targetRole == ssntp.UNKNOWN {
		subsetOfNodes = allNodes
	} else {
		trimmedNodes, err := trimComputeNodes(c, allNodes, targetRole)
		if err != nil {
			return errorResponse(err), err
		}
		subsetOfNodes = trimmedNodes
	}

	nodeSummary, err := c.ds.GetNodeSummary()
	if err != nil {
		return errorResponse(err), err
	}

	for _, node := range nodeSummary {
		for i := range subsetOfNodes.Nodes {
			if subsetOfNodes.Nodes[i].ID != node.NodeID {
				continue
			}

			subsetOfNodes.Nodes[i].TotalInstances =
				node.TotalInstances
			subsetOfNodes.Nodes[i].TotalRunningInstances =
				node.TotalRunningInstances
			subsetOfNodes.Nodes[i].TotalPendingInstances =
				node.TotalPendingInstances
			subsetOfNodes.Nodes[i].TotalPausedInstances =
				node.TotalPausedInstances
		}
	}

	sort.Sort(types.SortedNodesByID(subsetOfNodes.Nodes))

	pager := nodePager{
		ctl:   c,
		nodes: subsetOfNodes.Nodes,
	}

	resp, err := pager.nextPage(none, "", r)
	if err != nil {
		return errorResponse(err), err
	}

	return APIResponse{http.StatusOK, resp}, nil
}

func listComputeNodes(c *controller, w http.ResponseWriter, r *http.Request) (APIResponse, error) {
	return listSubsetOfNodes(c, w, r, ssntp.AGENT)
}

func listNetworkNodes(c *controller, w http.ResponseWriter, r *http.Request) (APIResponse, error) {
	return listSubsetOfNodes(c, w, r, ssntp.NETAGENT)
}

func listNodes(c *controller, w http.ResponseWriter, r *http.Request) (APIResponse, error) {
	return listSubsetOfNodes(c, w, r, ssntp.UNKNOWN)
}

func listNodeServers(c *controller, w http.ResponseWriter, r *http.Request) (APIResponse, error) {
	vars := mux.Vars(r)
	nodeID := vars["node"]

	serversStats := c.ds.GetInstanceLastStats(nodeID)

	instances, err := c.ds.GetAllInstancesByNode(nodeID)
	if err != nil {
		return errorResponse(err), err
	}

	for _, instance := range instances {
		for i := range serversStats.Servers {
			if serversStats.Servers[i].ID != instance.ID {
				continue
			}

			serversStats.Servers[i].TenantID = instance.TenantID
			serversStats.Servers[i].IPv4 = instance.IPAddress
		}
	}

	pager := nodeServerPager{
		ctl:       c,
		instances: serversStats.Servers,
	}

	resp, err := pager.nextPage(none, "", r)
	if err != nil {
		return errorResponse(err), err
	}

	return APIResponse{http.StatusOK, resp}, nil
}

func listCNCIs(c *controller, w http.ResponseWriter, r *http.Request) (APIResponse, error) {
	var ciaoCNCIs types.CiaoCNCIs

	cncis, err := c.ds.GetTenantCNCISummary("")
	if err != nil {
		return errorResponse(err), err
	}

	for _, cnci := range cncis {
		var subnets []types.CiaoCNCISubnet

		if cnci.InstanceID == "" {
			continue
		}

		for _, subnet := range cnci.Subnets {
			subnets = append(subnets,
				types.CiaoCNCISubnet{
					Subnet: subnet,
				},
			)
		}

		ciaoCNCIs.CNCIs = append(ciaoCNCIs.CNCIs,
			types.CiaoCNCI{
				ID:       cnci.InstanceID,
				TenantID: cnci.TenantID,
				IPv4:     cnci.IPAddress,
				Subnets:  subnets,
			},
		)
	}

	return APIResponse{http.StatusOK, ciaoCNCIs}, nil
}

func listCNCIDetails(c *controller, w http.ResponseWriter, r *http.Request) (APIResponse, error) {
	vars := mux.Vars(r)
	cnciID := vars["cnci"]
	var ciaoCNCI types.CiaoCNCI

	cncis, err := c.ds.GetTenantCNCISummary(cnciID)
	if err != nil {
		return errorResponse(err), err
	}

	if len(cncis) > 0 {
		var subnets []types.CiaoCNCISubnet
		cnci := cncis[0]

		for _, subnet := range cnci.Subnets {
			subnets = append(subnets,
				types.CiaoCNCISubnet{
					Subnet: subnet,
				},
			)
		}

		ciaoCNCI = types.CiaoCNCI{
			ID:       cnci.InstanceID,
			TenantID: cnci.TenantID,
			IPv4:     cnci.IPAddress,
			Subnets:  subnets,
		}
	}

	return APIResponse{http.StatusOK, ciaoCNCI}, err
}

func listTraces(c *controller, w http.ResponseWriter, r *http.Request) (APIResponse, error) {
	var traces types.CiaoTracesSummary

	summaries, err := c.ds.GetBatchFrameSummary()
	if err != nil {
		return errorResponse(err), err
	}

	for _, s := range summaries {
		summary := types.CiaoTraceSummary{
			Label:     s.BatchID,
			Instances: s.NumInstances,
		}
		traces.Summaries = append(traces.Summaries, summary)
	}

	return APIResponse{http.StatusOK, traces}, err
}

func listEvents(c *controller, w http.ResponseWriter, r *http.Request) (APIResponse, error) {
	vars := mux.Vars(r)
	tenant := vars["tenant"]

	events := types.NewCiaoEvents()

	logs, err := c.ds.GetEventLog()
	if err != nil {
		return errorResponse(err), err
	}

	for _, l := range logs {
		if tenant != "" && tenant != l.TenantID {
			continue
		}

		event := types.CiaoEvent{
			Timestamp: l.Timestamp,
			TenantID:  l.TenantID,
			EventType: l.EventType,
			Message:   l.Message,
		}
		events.Events = append(events.Events, event)
	}

	return APIResponse{http.StatusOK, events}, err
}

func clearEvents(c *controller, w http.ResponseWriter, r *http.Request) (APIResponse, error) {
	err := c.ds.ClearLog()
	if err != nil {
		return errorResponse(err), err
	}

	return APIResponse{http.StatusAccepted, nil}, nil
}

func traceData(c *controller, w http.ResponseWriter, r *http.Request) (APIResponse, error) {
	vars := mux.Vars(r)
	label := vars["label"]
	var traceData types.CiaoTraceData

	batchStats, err := c.ds.GetBatchFrameStatistics(label)
	if err != nil {
		return errorResponse(err), err
	}

	traceData.Summary = types.CiaoBatchFrameStat{
		NumInstances:             batchStats[0].NumInstances,
		TotalElapsed:             batchStats[0].TotalElapsed,
		AverageElapsed:           batchStats[0].AverageElapsed,
		AverageControllerElapsed: batchStats[0].AverageControllerElapsed,
		AverageLauncherElapsed:   batchStats[0].AverageLauncherElapsed,
		AverageSchedulerElapsed:  batchStats[0].AverageSchedulerElapsed,
		VarianceController:       batchStats[0].VarianceController,
		VarianceLauncher:         batchStats[0].VarianceLauncher,
		VarianceScheduler:        batchStats[0].VarianceScheduler,
	}

	return APIResponse{http.StatusOK, traceData}, nil
}
