package main

import (
	"code.justin.tv/dta/necronomicon-user-api/app"
	"encoding/json"
	"fmt"
	_ "github.com/lib/pq"
	"reflect"
	"strconv"
	"strings"
	"time"
)

type SnapshotModel struct {
	ID        int                                                      `json:"id" db:"id"`
	User      string                                                   `json:"user" db:"user"`
	Message   string                                                   `json:"message" db:"message"`
	Timestamp time.Time                                                `json:"timestamp" db:"created_at"`
	Payload   app.TwitchDtaNecronomiconUserapiSnapshotChangeCollection `json:"payload" db:"changes"`
	State     map[string]app.TwitchDtaNecronomiconUserapiSnapshotState `json:"state" db:"state"`
}

type DeploymentModel struct {
	ID         int       `db:"id"`
	User       string    `db:"user"`
	Message    string    `db:"message"`
	Timestamp  time.Time `db:"created_at"`
	SnapshotID int       `db:"snapshot_id"`
}

// Given a state and changes in a snapshot, return a new state with the changes applied
func GetNewState(oldState *map[string]app.TwitchDtaNecronomiconUserapiSnapshotState, changes app.TwitchDtaNecronomiconUserapiSnapshotChangeCollection) (*map[string]app.TwitchDtaNecronomiconUserapiSnapshotState, error) {

	// First, copy the old state as is into a new state object
	newState := make(map[string]app.TwitchDtaNecronomiconUserapiSnapshotState)
	if oldState != nil {
		for nameSpace, configSection := range *oldState {
			newState[nameSpace] = app.TwitchDtaNecronomiconUserapiSnapshotState{
				Parent: configSection.Parent,
				Values: make(map[string]map[string]interface{}),
			}
			for valueName, valueData := range configSection.Values {
				newState[nameSpace].Values[valueName] = valueData
			}
		}
	}

	// Now we go through the changes and start applying them to the new state
	for _, change := range changes {
		if change.Operation == "modified" {
			// A set operation will replace a whole config tree for a namespace
			if change.Parent != nil {
				if _, ok := newState[*change.Parent]; !ok {
					return nil, fmt.Errorf("Setting parent of '%s' to '%s', but there is no environment for '%s' (%#v)", change.Name, *change.Parent, *change.Parent, newState)
				}
			}
			configSection := app.TwitchDtaNecronomiconUserapiSnapshotState{
				Parent: change.Parent,
				Values: make(map[string]map[string]interface{})}
			for nameSpace, configValues := range change.Values {
				configSection.Values[nameSpace] = make(map[string]interface{})
				for valueName, valueData := range configValues {
					configSection.Values[nameSpace][valueName] = valueData
				}
			}
			newState[change.Name] = configSection
		} else if change.Operation == "deleted" {
			delete(newState, change.Name)
		}
	}
	return &newState, nil
}

// Given a snapshot state, return the entire state merged.
func GetMergedState(state *map[string]app.TwitchDtaNecronomiconUserapiSnapshotState) (map[string]app.TwitchDtaNecronomiconUserapiSnapshotState, error) {
	stateMap := *state
	mergedState := make(map[string]app.TwitchDtaNecronomiconUserapiSnapshotState)
	// first we copy all the root level configs as-is (no need to merge)
	for envName, env := range stateMap {
		if env.Parent == nil {
			mergedState[envName] = app.TwitchDtaNecronomiconUserapiSnapshotState{
				Values: make(map[string]map[string]interface{}),
			}
			for nameSpace, configSection := range env.Values {
				mergedState[envName].Values[nameSpace] = configSection
			}
			delete(stateMap, envName)
		}
	}

	// now we iterate by depth level
	for len(stateMap) > 0 {
		statePreSize := len(stateMap)
		for envName, env := range stateMap {
			if parentVal, ok := mergedState[*env.Parent]; ok {
				// We found the parent in the merged map
				mergedEnv := app.TwitchDtaNecronomiconUserapiSnapshotState{
					Parent: env.Parent,
					Values: make(map[string]map[string]interface{}),
				}
				mergedState[envName] = mergedEnv

				for nameSpace, configSection := range parentVal.Values {
					mergedEnv.Values[nameSpace] = make(map[string]interface{})
					// copy config from parent
					for valName, valValue := range configSection {
						mergedEnv.Values[nameSpace][valName] = valValue
					}
				}
				for nameSpace, configSection := range env.Values {
					// copy config from current section, overwriting
					// anything the parent had if there are conflicts
					_, ok = mergedEnv.Values[nameSpace]
					if !ok {
						mergedEnv.Values[nameSpace] = make(map[string]interface{})
					}
					for valName, valValue := range configSection {
						mergedEnv.Values[nameSpace][valName] = valValue
					}
				}
				delete(stateMap, envName)
			}
		}
		if statePreSize == len(stateMap) {
			return mergedState, fmt.Errorf("State is corrupt, parents are lost")
		}
	}
	return mergedState, nil
}

// Efficient way to verify if a table is empty
func isTableEmpty(tx *Tx, table string) (isEmpty bool, err error) {
	var hasRows int
	row := tx.QueryRow("select1"+strings.Title(table), fmt.Sprintf("SELECT COUNT(1) FROM (SELECT 1 FROM %s LIMIT 1) AS t;", table))
	err = row.Scan(&hasRows)
	if err != nil {
		return
	}
	isEmpty = hasRows == 0
	return
}

// Handle the creation of a new snapshot in a transactional way
func (db *DB) CreateSnapshot(s *SnapshotModel) (id int, err error) {
	var newState, currentState *map[string]app.TwitchDtaNecronomiconUserapiSnapshotState
	var currentStateAsString []byte

	changesString, _ := json.Marshal(s.Payload)
	tx, err := db.GetNewTx("createSnapshot")
	if err != nil {
		return
	}
	defer func() { tx.Rollback(err) }() // use closure to defer evaluation of err

	_, err = tx.Exec("lockTable", "LOCK TABLE snapshots IN SHARE ROW EXCLUSIVE MODE;")
	if err != nil {
		return
	}

	isEmpty, err := isTableEmpty(tx, "snapshots")
	if err != nil {
		return
	}
	if isEmpty {
		newState, err = GetNewState(nil, s.Payload)
	} else {
		row := tx.QueryRow("selectLatestSnapshot", "SELECT state FROM snapshots ORDER BY created_at DESC LIMIT 1")
		err = row.Scan(&currentStateAsString)
		if err != nil {
			return
		}
		err = json.Unmarshal(currentStateAsString, &currentState)
		if err != nil {
			return
		}
		newState, err = GetNewState(currentState, s.Payload)
	}
	if err != nil {
		return
	}
	states, err := json.Marshal(newState)
	if err != nil {
		return
	}
	row := tx.QueryRow(
		"insertSnapshot",
		"INSERT INTO snapshots (\"user\", message, changes, state, created_at) VALUES ($1, $2, $3, $4, $5) RETURNING id",
		s.User, s.Message, string(changesString), string(states), s.Timestamp)
	err = row.Scan(&id)
	if err != nil {
		return
	}
	err = tx.Commit()
	return
}

// Return snapshot information for a given snapshot id
func (db *DB) GetSnapshot(ID int) (*SnapshotModel, error) {
	var user, message, changesMarshalled, stateMarshalled string
	var created_at time.Time
	var payload app.TwitchDtaNecronomiconUserapiSnapshotChangeCollection
	state := make(map[string]app.TwitchDtaNecronomiconUserapiSnapshotState)
	row := db.QueryRowx(
		"getSnapshot",
		"selectSnapshot",
		"SELECT \"user\", message, created_at, changes, state FROM snapshots WHERE id=$1",
		ID)
	err := row.Scan(&user, &message, &created_at, &changesMarshalled, &stateMarshalled)
	if err != nil {
		return nil, err
	}
	err = json.Unmarshal([]byte(changesMarshalled), &payload)
	if err != nil {
		return nil, err
	}
	err = json.Unmarshal([]byte(stateMarshalled), &state)
	if err != nil {
		return nil, err
	}
	snapshot := SnapshotModel{
		ID:        ID,
		User:      user,
		Message:   message,
		Timestamp: created_at,
		Payload:   payload,
		State:     state,
	}
	return &snapshot, nil
}

// Given a snapshot id and an environment name, return the merged configuration for the environment
func GetMergedEnvironment(snapshot *SnapshotModel, environment string) (state *app.TwitchDtaNecronomiconUserapiSnapshotState, err error) {
	state = nil
	mergedState, err := GetMergedState(&snapshot.State)
	if err != nil {
		return
	}
	if val, ok := mergedState[environment]; ok {
		state = &val
	}
	return
}

// Return a limited list of snapshots, optionally filtered by time. Sorted by time, more recents first.
func (db *DB) GetSnapshots(from *time.Time, to *time.Time, limit int) (snapshots []SnapshotModel, err error) {
	queryBase := "SELECT id, \"user\", message, created_at FROM snapshots"
	query, args := queryBuilderFromToLimit(queryBase, from, to, limit)
	err = db.Select("getSnapshots", "selectSnapshots", &snapshots, query, args...)
	return
}

// Return a limited list of deployments, optionally filtered by time. Sorted by time, more recents first.
func (db *DB) GetDeployments(from *time.Time, to *time.Time, limit int) (deployments []DeploymentModel, err error) {
	queryBase := "SELECT id, \"user\", message, created_at, snapshot_id FROM deployments"
	query, args := queryBuilderFromToLimit(queryBase, from, to, limit)
	err = db.Select("getDeployments", "selectDeployments", &deployments, query, args...)
	return
}

// Helper to build a query that receives optional from, to and limit args.
func queryBuilderFromToLimit(queryBase string, from *time.Time, to *time.Time, limit int) (query string, args []interface{}) {
	query = queryBase
	argpos := 1
	if from != nil || to != nil {
		query += " WHERE"
		if from != nil {
			query += " created_at >= $" + strconv.Itoa(argpos)
			argpos += 1
			args = append(args, from)
			if to != nil {
				query += " AND"
			}
		}
		if to != nil {
			query += " created_at <= $" + strconv.Itoa(argpos)
			argpos += 1
			args = append(args, to)
		}
	}
	query += " ORDER BY created_at DESC LIMIT $" + strconv.Itoa(argpos)
	args = append(args, limit)
	return
}

// Return snapshot information for multiple given snapshot ids
func (db *DB) GetSnapshotMultiple(ID []int) (snapshots map[int]SnapshotModel, err error) {
	snapshots = make(map[int]SnapshotModel)
	var IDStrings []string
	for _, i := range ID {
		IDStrings = append(IDStrings, strconv.Itoa(i))
	}
	rows, err := db.Queryx(
		"getSnapshotMultiple",
		"selectSnapshots",
		"SELECT id, \"user\", message, created_at, changes, state FROM snapshots WHERE id=ANY($1::integer[])",
		fmt.Sprintf("{%s}", strings.Join(IDStrings, ",")))
	if err != nil {
		return
	}
	for rows.Next() {
		var s SnapshotModel
		var id int
		var user, message, changesMarshalled, stateMarshalled string
		var createdAt time.Time
		var payload app.TwitchDtaNecronomiconUserapiSnapshotChangeCollection
		state := make(map[string]app.TwitchDtaNecronomiconUserapiSnapshotState)

		err = rows.Scan(&id, &user, &message, &createdAt, &changesMarshalled, &stateMarshalled)
		if err != nil {
			return
		}
		err = json.Unmarshal([]byte(changesMarshalled), &payload)
		if err != nil {
			return
		}
		err = json.Unmarshal([]byte(stateMarshalled), &state)
		if err != nil {
			return
		}
		s = SnapshotModel{
			User:      user,
			Message:   message,
			Timestamp: createdAt,
			Payload:   payload,
			State:     state,
		}
		snapshots[id] = s
	}
	return
}

// Return a deep copy of the state, leveraging json marshalling for copying
func GetStateCopy(state app.TwitchDtaNecronomiconUserapiSnapshotState) (*app.TwitchDtaNecronomiconUserapiSnapshotState, error) {
	dup := app.TwitchDtaNecronomiconUserapiSnapshotState{}
	s, err := json.Marshal(state)
	if err != nil {
		return nil, err
	}
	err = json.Unmarshal([]byte(s), &dup)
	return &dup, err
}

// given 2 string pointers
func equalStringPointers(s1, s2 *string) bool {
	if s1 == nil && s2 == nil {
		return true
	}
	if s1 != nil && s2 != nil && *s1 == *s2 {
		return true
	}
	return false
}

// Get a map of configuration values with environment names as the key by comparing two snapshots
func GetChangedEnvironmentsInStates(previousState map[string]app.TwitchDtaNecronomiconUserapiSnapshotState, currentState map[string]app.TwitchDtaNecronomiconUserapiSnapshotState) (environments app.TwitchDtaNecronomiconUserapiSnapshotStatemap, err error) {
	var c *app.TwitchDtaNecronomiconUserapiSnapshotState
	environments = app.TwitchDtaNecronomiconUserapiSnapshotStatemap{}
	environments.Map = map[string]*app.TwitchDtaNecronomiconUserapiSnapshotState{}
	for environment := range currentState {
		config := currentState[environment]
		previousConfig, ok := previousState[environment]
		if !ok || !equalStringPointers(config.Parent, previousConfig.Parent) || (!reflect.DeepEqual(config.Values, previousConfig.Values)) {
			c, err = GetStateCopy(config)
			if err != nil {
				return
			}
			environments.Map[environment] = c
			continue
		}
	}
	return
}

// Handle the creation of a new deployment in a transactional way
func (db *DB) CreateDeployment(d *DeploymentModel) (id int, err error) {
	row := db.QueryRowx(
		"createDeployment",
		"insertDeployment",
		"INSERT INTO deployments (\"user\", message, snapshot_id, created_at) VALUES ($1, $2, $3, $4) RETURNING id",
		d.User, d.Message, d.SnapshotID, d.Timestamp)
	err = row.Scan(&id)
	return
}

// Given a deployment id, get a deployment
func (db *DB) GetDeployment(id int) (deployment DeploymentModel, err error) {
	err = db.Get(
		"getDeployment",
		"selectDeployment",
		&deployment,
		"SELECT id, \"user\", message, snapshot_id, created_at FROM deployments WHERE id=$1",
		id)
	return
}

// Return deployment information for the latest deployment
func (db *DB) GetLatestDeployment() (deployment *DeploymentModel, err error) {
	deployment = &DeploymentModel{}
	err = db.Get("getLatestDeployment", "selectDeployment", deployment, "SELECT \"user\", message, snapshot_id, created_at FROM deployments ORDER BY ID DESC LIMIT 1")
	return
}
