package main

import (
	"code.justin.tv/dta/necronomicon-user-api/app"
	"database/sql"
	"encoding/json"
	"fmt"
	"github.com/goadesign/goa"
	"github.com/goamz/goamz/s3"
	"math"
	"time"
)

var invalidSnapshotErr = goa.NewErrorClass("invalid_snapshot", 400)

// DeploymentController implements the deployment resource.
type DeploymentController struct {
	*goa.Controller
	resources *Resources
}

type BucketPutter interface {
	Put(path string, data []byte, contType string, perm s3.ACL, options s3.Options) error
}

type DeploymentDoc struct {
	Version int                    `json:"version"`
	ID      int                    `json:"id"`
	Updated []DeploymentDocUpdated `json:"updated"`
}

type DeploymentDocUpdated struct {
	Environment string   `json:"environment"`
	Namespaces  []string `json:"namespaces"`
}

type EnvironmentDoc struct {
	Version     int                               `json:"version"`
	ID          int                               `json:"id"`
	Environment string                            `json:"environment"`
	Namespaces  map[string]map[string]interface{} `json:"namespaces"`
}

// NewDeploymentController creates a deployment controller.
func NewDeploymentController(service *goa.Service) *DeploymentController {
	return &DeploymentController{
		Controller: service.NewController("DeploymentController"),
	}
}

func getChangedEnvironments(db *DB, snapshotID int) (res *app.TwitchDtaNecronomiconUserapiSnapshotStatemap, err error) {
	snapshotIDs := []int{snapshotID}

	previousDeployment, err := db.GetLatestDeployment()
	if err != nil && err != sql.ErrNoRows {
		return nil, err
	}
	if previousDeployment != nil {
		snapshotIDs = append(snapshotIDs, previousDeployment.SnapshotID)
	}
	snapshots, err := db.GetSnapshotMultiple(snapshotIDs)
	if err != nil {
		return nil, err
	}
	_, ok := snapshots[snapshotID]
	if !ok {
		return nil, invalidSnapshotErr("Snapshot does not exist", "snapshot_id", snapshotID)
	}

	var previousState map[string]app.TwitchDtaNecronomiconUserapiSnapshotState
	if previousDeployment != nil {
		previousState = snapshots[previousDeployment.SnapshotID].State
	}
	previousStateMerged, err := GetMergedState(&previousState)
	if err != nil {
		return nil, err
	}
	currentState := snapshots[snapshotID].State
	currentStateMerged, err := GetMergedState(&currentState)
	if err != nil {
		return nil, err
	}
	environmentsChanged, err := GetChangedEnvironmentsInStates(previousStateMerged, currentStateMerged)
	if err != nil {
		return nil, err
	}
	return &environmentsChanged, nil

}

// Create runs the create action.
func (c *DeploymentController) Create(ctx *app.CreateDeploymentContext) error {
	// DeploymentController_Create: start_implement
	resources := c.resources
	db, bucket := resources.db, resources.bucket

	environmentsChanged, err := getChangedEnvironments(db, ctx.Payload.SnapshotID)
	if err != nil {
		return ctx.BadRequest(goa.ErrBadRequest(err))
	}

	s3PostError := goa.NewErrorClass("s3_post_error", 400)
	user, err := getUserFromBasicAuthString(ctx.RequestData)
	if err != nil {
		return err
	}
	deployment := &DeploymentModel{
		User:       user,
		Message:    ctx.Payload.Message,
		Timestamp:  time.Now(),
		SnapshotID: ctx.Payload.SnapshotID,
	}
	id, err := db.CreateDeployment(deployment)
	if err == sql.ErrNoRows {
		return ctx.BadRequest(goa.ErrBadRequest(invalidSnapshotErr("Snapshot does not exist", "snapshot_id", ctx.Payload.SnapshotID)))
	}
	if err != nil {
		return ctx.BadRequest(goa.ErrBadRequest(err))
	}
	err = publishDeployment(bucket, id, environmentsChanged.Map)
	if err != nil {
		return ctx.BadRequest(goa.ErrBadRequest(s3PostError("Couldn't publish to S3", "snapshot_id", ctx.Payload.SnapshotID, "error", err)))
	}

	ctx.ResponseData.Header().Set("Location", app.DeploymentHref(id))
	return ctx.Created()
	// DeploymentController_Create: end_implement
}

func publishDeployment(bucket BucketPutter, id int, environmentsChanged map[string]*app.TwitchDtaNecronomiconUserapiSnapshotState) error {
	acl := s3.PublicRead
	var options s3.Options

	doc := &DeploymentDoc{
		Version: DocumentFormatVersion,
		ID:      id,
		Updated: []DeploymentDocUpdated{},
	}

	for name, environment := range environmentsChanged {
		environmentMarshaled, err := json.Marshal(&EnvironmentDoc{
			Version:     DocumentFormatVersion,
			ID:          id,
			Environment: name,
			Namespaces:  environment.Values,
		})
		if err != nil {
			return err
		}
		err = bucket.Put(fmt.Sprintf("environments/%s/%s", name, formatIdString(id)), environmentMarshaled, "application/json", acl, options)
		if err != nil {
			return err
		}

		namespaces := []string{}
		for ns, _ := range environment.Values {
			namespaces = append(namespaces, ns)
		}
		doc.Updated = append(doc.Updated, DeploymentDocUpdated{Environment: name, Namespaces: namespaces})
	}
	docMarshaled, err := json.Marshal(doc)
	if err != nil {
		return err
	}
	err = bucket.Put(fmt.Sprintf("deployments/%s", formatIdString(id)), docMarshaled, "application/json", acl, options)
	if err != nil {
		return err
	}
	return nil
}

func formatIdString(id int) string {
	return fmt.Sprintf("%0x-%d", math.MaxInt32-id, id)
}

// List runs the list action.
func (c *DeploymentController) List(ctx *app.ListDeploymentContext) error {
	resources := c.resources
	db := resources.db
	res := app.TwitchDtaNecronomiconUserapiDeploymentCollection{}
	deployments, err := db.GetDeployments(ctx.From, ctx.To, ctx.Limit)
	if err != nil {
		return err
	}
	targets := make(map[string][]string)
	agents := make([]*app.Agent, 0, 10)
	for _, deployment := range deployments {
		res = append(res, &app.TwitchDtaNecronomiconUserapiDeployment{
			Message:      deployment.Message,
			DeploymentID: deployment.ID,
			Timestamp:    deployment.Timestamp,
			User:         deployment.User,
			SnapshotID:   deployment.SnapshotID,
			Targets:      targets,
			Agents:       agents,
		})
	}
	return ctx.OK(res)
}

// Preview runs the preview action.
func (c *DeploymentController) Preview(ctx *app.PreviewDeploymentContext) error {
	// DeploymentController_Preview: start_implement

	resources := c.resources
	db := resources.db

	envMap, err := getChangedEnvironments(db, ctx.Payload.SnapshotID)
	if err != nil {
		return ctx.BadRequest(goa.ErrBadRequest(err))
	}

	// DeploymentController_Preview: end_implement
	return ctx.OK(envMap)
}

// Show runs the show action.
func (c *DeploymentController) Show(ctx *app.ShowDeploymentContext) error {
	resources := c.resources
	db := resources.db
	d, err := db.GetDeployment(ctx.DeploymentID)
	if err == sql.ErrNoRows {
		return ctx.NotFound()
	}
	if err != nil {
		return err
	}
	targets := make(map[string][]string)
	agents := make([]*app.Agent, 0, 10)
	res := &app.TwitchDtaNecronomiconUserapiDeployment{
		DeploymentID: d.ID,
		Message:      d.Message,
		SnapshotID:   d.SnapshotID,
		Timestamp:    d.Timestamp,
		User:         d.User,
		Targets:      targets,
		Agents:       agents,
	}
	return ctx.OK(res)
}
