// Copyright 2015 clair authors
//
// 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 database

import (
	"strconv"

	"github.com/coreos/clair/utils"
	cerrors "github.com/coreos/clair/utils/errors"
	"github.com/google/cayley"
	"github.com/google/cayley/graph"
	"github.com/google/cayley/graph/path"
)

const (
	FieldLayerIsValue           = "layer"
	FieldLayerID                = "id"
	FieldLayerParent            = "parent"
	FieldLayerSuccessors        = "successors"
	FieldLayerOS                = "os"
	FieldLayerInstalledPackages = "adds"
	FieldLayerRemovedPackages   = "removes"
	FieldLayerEngineVersion     = "engineVersion"

	FieldLayerPackages = "adds/removes"
)

var FieldLayerAll = []string{FieldLayerID, FieldLayerParent, FieldLayerSuccessors, FieldLayerOS, FieldLayerPackages, FieldLayerEngineVersion}

// Layer represents an unique container layer
type Layer struct {
	Node                   string `json:"-"`
	ID                     string
	ParentNode             string   `json:"-"`
	SuccessorsNodes        []string `json:"-"`
	OS                     string
	InstalledPackagesNodes []string `json:"-"`
	RemovedPackagesNodes   []string `json:"-"`
	EngineVersion          int
}

// GetNode returns the node name of a Layer
// Requires the key field: ID
func (l *Layer) GetNode() string {
	return FieldLayerIsValue + ":" + utils.Hash(l.ID)
}

// InsertLayer insert a single layer in the database
//
// ID, and EngineVersion fields are required.
// ParentNode, OS, InstalledPackagesNodes and RemovedPackagesNodes are optional,
// SuccessorsNodes is unnecessary.
//
// The ID MUST be unique for two different layers.
//
//
// If the Layer already exists, nothing is done, except if the provided engine
// version is higher than the existing one, in which case, the OS,
// InstalledPackagesNodes and RemovedPackagesNodes fields will be replaced.
//
// The layer should only contains the newly installed/removed packages
// There is no safeguard that prevents from marking a package as newly installed
// while it has already been installed in one of its parent.
func InsertLayer(layer *Layer) error {
	// Verify parameters
	if layer.ID == "" {
		log.Warning("could not insert a layer which has an empty ID")
		return cerrors.NewBadRequestError("could not insert a layer which has an empty ID")
	}

	// Create required data structures
	t := cayley.NewTransaction()
	layer.Node = layer.GetNode()

	// Try to find an existing layer
	existingLayer, err := FindOneLayerByNode(layer.Node, FieldLayerAll)
	if err != nil && err != cerrors.ErrNotFound {
		return err
	}

	if existingLayer != nil && existingLayer.EngineVersion >= layer.EngineVersion {
		// The layer exists and has an equal or higher engine verison, do nothing
		return nil
	}

	if existingLayer == nil {
		// Create case: add permanent nodes
		t.AddQuad(cayley.Quad(layer.Node, FieldIs, FieldLayerIsValue, ""))
		t.AddQuad(cayley.Quad(layer.Node, FieldLayerID, layer.ID, ""))
		t.AddQuad(cayley.Quad(layer.Node, FieldLayerParent, layer.ParentNode, ""))
	} else {
		// Update case: remove everything before we add updated data
		t.RemoveQuad(cayley.Quad(layer.Node, FieldLayerOS, existingLayer.OS, ""))
		for _, pkg := range existingLayer.InstalledPackagesNodes {
			t.RemoveQuad(cayley.Quad(layer.Node, FieldLayerInstalledPackages, pkg, ""))
		}
		for _, pkg := range existingLayer.RemovedPackagesNodes {
			t.RemoveQuad(cayley.Quad(layer.Node, FieldLayerRemovedPackages, pkg, ""))
		}
		t.RemoveQuad(cayley.Quad(layer.Node, FieldLayerEngineVersion, strconv.Itoa(existingLayer.EngineVersion), ""))
	}

	// Add OS/Packages
	t.AddQuad(cayley.Quad(layer.Node, FieldLayerOS, layer.OS, ""))
	for _, pkg := range layer.InstalledPackagesNodes {
		t.AddQuad(cayley.Quad(layer.Node, FieldLayerInstalledPackages, pkg, ""))
	}
	for _, pkg := range layer.RemovedPackagesNodes {
		t.AddQuad(cayley.Quad(layer.Node, FieldLayerRemovedPackages, pkg, ""))
	}
	t.AddQuad(cayley.Quad(layer.Node, FieldLayerEngineVersion, strconv.Itoa(layer.EngineVersion), ""))

	// Apply transaction
	if err = store.ApplyTransaction(t); err != nil {
		log.Errorf("failed transaction (InsertLayer): %s", err)
		return ErrTransaction
	}

	return nil
}

// FindOneLayerByID finds and returns a single layer having the given ID,
// selecting the specified fields and hardcoding its ID
func FindOneLayerByID(ID string, selectedFields []string) (*Layer, error) {
	t := &Layer{ID: ID}
	l, err := FindOneLayerByNode(t.GetNode(), selectedFields)
	if err != nil {
		return nil, err
	}
	l.ID = ID
	return l, nil
}

// FindOneLayerByNode finds and returns a single package by its node, selecting the specified fields
func FindOneLayerByNode(node string, selectedFields []string) (*Layer, error) {
	l, err := toLayers(cayley.StartPath(store, node).Has(FieldIs, FieldLayerIsValue), selectedFields)
	if err != nil {
		return nil, err
	}
	if len(l) == 1 {
		return l[0], nil
	}
	if len(l) > 1 {
		log.Errorf("found multiple layers with identical node [Node: %s]", node)
		return nil, ErrInconsistent
	}

	return nil, cerrors.ErrNotFound
}

// FindAllLayersByAddedPackageNodes finds and returns all layers that add the
// given packages (by their nodes), selecting the specified fields
func FindAllLayersByAddedPackageNodes(nodes []string, selectedFields []string) ([]*Layer, error) {
	layers, err := toLayers(cayley.StartPath(store, nodes...).In(FieldLayerInstalledPackages), selectedFields)
	if err != nil {
		return []*Layer{}, err
	}
	return layers, nil
}

// FindAllLayersByPackageNode finds and returns all layers that have the given package (by its node), selecting the specified fields
// func FindAllLayersByPackageNode(node string, only map[string]struct{}) ([]*Layer, error) {
// 	var layers []*Layer
//
// 	// We need the successors field
// 	if only != nil {
// 		only[FieldLayerSuccessors] = struct{}{}
// 	}
//
// 	// Get all the layers which remove the package
// 	layersNodesRemoving, err := toValues(cayley.StartPath(store, node).In(FieldLayerRemovedPackages).Has(FieldIs, FieldLayerIsValue))
// 	if err != nil {
// 		return []*Layer{}, err
// 	}
// 	layersNodesRemovingMap := make(map[string]struct{})
// 	for _, l := range layersNodesRemoving {
// 		layersNodesRemovingMap[l] = struct{}{}
// 	}
//
// 	layersToBrowse, err := toLayers(cayley.StartPath(store, node).In(FieldLayerInstalledPackages).Has(FieldIs, FieldLayerIsValue), only)
// 	if err != nil {
// 		return []*Layer{}, err
// 	}
// 	for len(layersToBrowse) > 0 {
// 		var newLayersToBrowse []*Layer
// 		for _, layerToBrowse := range layersToBrowse {
// 			if _, layerRemovesPackage := layersNodesRemovingMap[layerToBrowse.Node]; !layerRemovesPackage {
// 				layers = append(layers, layerToBrowse)
// 				successors, err := layerToBrowse.Successors(only)
// 				if err != nil {
// 					return []*Layer{}, err
// 				}
// 				newLayersToBrowse = append(newLayersToBrowse, successors...)
// 			}
// 			layersToBrowse = newLayersToBrowse
// 		}
// 	}
//
// 	return layers, nil
// }

// toLayers converts a path leading to one or multiple layers to Layer structs,
// selecting the specified fields
func toLayers(path *path.Path, selectedFields []string) ([]*Layer, error) {
	var layers []*Layer

	saveFields(path, selectedFields, []string{FieldLayerSuccessors, FieldLayerPackages, FieldLayerInstalledPackages, FieldLayerRemovedPackages})
	it, _ := path.BuildIterator().Optimize()
	defer it.Close()
	for cayley.RawNext(it) {
		tags := make(map[string]graph.Value)
		it.TagResults(tags)

		layer := Layer{Node: store.NameOf(it.Result())}
		for _, selectedField := range selectedFields {
			switch selectedField {
			case FieldLayerID:
				layer.ID = store.NameOf(tags[FieldLayerID])
			case FieldLayerParent:
				layer.ParentNode = store.NameOf(tags[FieldLayerParent])
			case FieldLayerSuccessors:
				var err error
				layer.SuccessorsNodes, err = toValues(cayley.StartPath(store, layer.Node).In(FieldLayerParent))
				if err != nil {
					log.Errorf("could not get successors of layer %s: %s.", layer.Node, err.Error())
					return nil, err
				}
			case FieldLayerOS:
				layer.OS = store.NameOf(tags[FieldLayerOS])
			case FieldLayerPackages:
				var err error
				it, _ := cayley.StartPath(store, layer.Node).OutWithTags([]string{"predicate"}, FieldLayerInstalledPackages, FieldLayerRemovedPackages).BuildIterator().Optimize()
				defer it.Close()
				for cayley.RawNext(it) {
					tags := make(map[string]graph.Value)
					it.TagResults(tags)

					predicate := store.NameOf(tags["predicate"])
					if predicate == FieldLayerInstalledPackages {
						layer.InstalledPackagesNodes = append(layer.InstalledPackagesNodes, store.NameOf(it.Result()))
					} else if predicate == FieldLayerRemovedPackages {
						layer.RemovedPackagesNodes = append(layer.RemovedPackagesNodes, store.NameOf(it.Result()))
					}
				}
				if it.Err() != nil {
					log.Errorf("could not get installed/removed packages of layer %s: %s.", layer.Node, it.Err())
					return nil, err
				}
			case FieldLayerEngineVersion:
				layer.EngineVersion, _ = strconv.Atoi(store.NameOf(tags[FieldLayerEngineVersion]))
			default:
				panic("unknown selectedField")
			}
		}
		layers = append(layers, &layer)
	}
	if it.Err() != nil {
		log.Errorf("failed query in toLayers: %s", it.Err())
		return []*Layer{}, ErrBackendException
	}

	return layers, nil
}

// Successors find and returns all layers that define l as their parent,
// selecting the specified fields
// It requires that FieldLayerSuccessors field has been selected on l
// func (l *Layer) Successors(selectedFields []string) ([]*Layer, error) {
// 	if len(l.SuccessorsNodes) == 0 {
// 		return []*Layer{}, nil
// 	}
//
// 	return toLayers(cayley.StartPath(store, l.SuccessorsNodes...), only)
// }

// Parent find and returns the parent layer of l, selecting the specified fields
// It requires that FieldLayerParent field has been selected on l
func (l *Layer) Parent(selectedFields []string) (*Layer, error) {
	if l.ParentNode == "" {
		return nil, nil
	}

	parent, err := toLayers(cayley.StartPath(store, l.ParentNode), selectedFields)
	if err != nil {
		return nil, err
	}
	if len(parent) == 1 {
		return parent[0], nil
	}
	if len(parent) > 1 {
		log.Errorf("found multiple layers when getting parent layer of layer %s", l.ParentNode)
		return nil, ErrInconsistent
	}
	return nil, nil
}

// Sublayers find and returns all layers that compose l, selecting the specified
// fields
// It requires that FieldLayerParent field has been selected on l
// The base image comes first, and l is last
// func (l *Layer) Sublayers(selectedFields []string) ([]*Layer, error) {
// 	var sublayers []*Layer
//
// 	// We need the parent field
// 	if only != nil {
// 		only[FieldLayerParent] = struct{}{}
// 	}
//
// 	parent, err := l.Parent(only)
// 	if err != nil {
// 		return []*Layer{}, err
// 	}
// 	if parent != nil {
// 		parentSublayers, err := parent.Sublayers(only)
// 		if err != nil {
// 			return []*Layer{}, err
// 		}
// 		sublayers = append(sublayers, parentSublayers...)
// 	}
//
// 	sublayers = append(sublayers, l)
//
// 	return sublayers, nil
// }

// AllPackages computes the full list of packages that l has and return them as
// nodes.
// It requires that FieldLayerParent, FieldLayerContentInstalledPackages,
// FieldLayerContentRemovedPackages fields has been selected on l
func (l *Layer) AllPackages() ([]string, error) {
	var allPackages []string

	parent, err := l.Parent([]string{FieldLayerParent, FieldLayerPackages})
	if err != nil {
		return []string{}, err
	}
	if parent != nil {
		allPackages, err = parent.AllPackages()
		if err != nil {
			return []string{}, err
		}
	}

	return append(utils.CompareStringLists(allPackages, l.RemovedPackagesNodes), l.InstalledPackagesNodes...), nil
}

// OperatingSystem tries to find the Operating System of a layer using its
// parents.
// It requires that FieldLayerParent and FieldLayerOS fields has been
// selected on l
func (l *Layer) OperatingSystem() (string, error) {
	if l.OS != "" {
		return l.OS, nil
	}

	// Try from the parent
	parent, err := l.Parent([]string{FieldLayerParent, FieldLayerOS})
	if err != nil {
		return "", err
	}
	if parent != nil {
		return parent.OperatingSystem()
	}
	return "", nil
}