package graphview

import (
	"sort"

	"github.com/gonum/graph"

	buildedges "github.com/openshift/oc/pkg/helpers/graph/buildgraph"
	buildgraph "github.com/openshift/oc/pkg/helpers/graph/buildgraph/nodes"
	osgraph "github.com/openshift/oc/pkg/helpers/graph/genericgraph"
	imageedges "github.com/openshift/oc/pkg/helpers/graph/imagegraph"
	imagegraph "github.com/openshift/oc/pkg/helpers/graph/imagegraph/nodes"
)

// ImagePipeline represents a build, its output, and any inputs. The input
// to a build may be another ImagePipeline.
type ImagePipeline struct {
	Image               ImageTagLocation
	DestinationResolved bool
	ScheduledImport     bool

	Build *buildgraph.BuildConfigNode

	LastSuccessfulBuild   *buildgraph.BuildNode
	LastUnsuccessfulBuild *buildgraph.BuildNode
	ActiveBuilds          []*buildgraph.BuildNode

	// If set, the base image used by the build
	BaseImage ImageTagLocation
	// if set, the build config names that produces the base image
	BaseBuilds []string
	// If set, the source repository that inputs to the build
	Source SourceLocation
}

// ImageTagLocation identifies the source or destination of an image. Represents
// both a tag in a container image repository, as well as a tag in an OpenShift image stream.
type ImageTagLocation interface {
	ID() int
	ImageSpec() string
	ImageTag() string
}

// SourceLocation identifies a repository that is an input to a build.
type SourceLocation interface {
	ID() int
}

func AllImagePipelinesFromBuildConfig(g osgraph.Graph, excludeNodeIDs IntSet) ([]ImagePipeline, IntSet) {
	covered := IntSet{}
	pipelines := []ImagePipeline{}

	for _, uncastNode := range g.NodesByKind(buildgraph.BuildConfigNodeKind) {
		if excludeNodeIDs.Has(uncastNode.ID()) {
			continue
		}

		pipeline, covers := NewImagePipelineFromBuildConfigNode(g, uncastNode.(*buildgraph.BuildConfigNode))
		covered.Insert(covers.List()...)
		pipelines = append(pipelines, pipeline)
	}

	sort.Sort(SortedImagePipelines(pipelines))

	outputImageToBCMap := make(map[string][]string)
	for _, pipeline := range pipelines {
		// note, bc does not have to have an output image
		if pipeline.Image != nil {
			bcs, ok := outputImageToBCMap[pipeline.Image.ImageSpec()]
			if !ok {
				bcs = []string{}
			}
			bcs = append(bcs, pipeline.Build.BuildConfig.Name)
			outputImageToBCMap[pipeline.Image.ImageSpec()] = bcs
		}
	}

	if len(outputImageToBCMap) > 0 {
		for i, pipeline := range pipelines {
			// note, bc does not have to have an input strategy image
			if pipeline.BaseImage != nil {
				baseBCs, ok := outputImageToBCMap[pipeline.BaseImage.ImageSpec()]
				if ok && len(baseBCs) > 0 {
					pipelines[i].BaseBuilds = baseBCs
				}
			}
		}
	}

	return pipelines, covered
}

// NewImagePipeline attempts to locate a build flow from the provided node. If no such
// build flow can be located, false is returned.
func NewImagePipelineFromBuildConfigNode(g osgraph.Graph, bcNode *buildgraph.BuildConfigNode) (ImagePipeline, IntSet) {
	covered := IntSet{}
	covered.Insert(bcNode.ID())

	flow := ImagePipeline{}

	base, src, coveredInputs, scheduled, _ := findBuildInputs(g, bcNode)
	covered.Insert(coveredInputs.List()...)
	flow.BaseImage = base
	flow.Source = src
	flow.Build = bcNode
	flow.ScheduledImport = scheduled
	flow.LastSuccessfulBuild, flow.LastUnsuccessfulBuild, flow.ActiveBuilds = buildedges.RelevantBuilds(g, flow.Build)
	flow.Image = findBuildOutput(g, bcNode)

	// we should have at most one
	for _, buildOutputNode := range g.SuccessorNodesByEdgeKind(bcNode, buildedges.BuildOutputEdgeKind) {
		// this will handle the imagestream tag case
		for _, input := range g.SuccessorNodesByEdgeKind(buildOutputNode, imageedges.ReferencedImageStreamGraphEdgeKind) {
			imageStreamNode := input.(*imagegraph.ImageStreamNode)

			flow.DestinationResolved = (len(imageStreamNode.Status.DockerImageRepository) != 0)
		}
		// this will handle the imagestream image case
		for _, input := range g.SuccessorNodesByEdgeKind(buildOutputNode, imageedges.ReferencedImageStreamImageGraphEdgeKind) {
			imageStreamNode := input.(*imagegraph.ImageStreamNode)

			flow.DestinationResolved = (len(imageStreamNode.Status.DockerImageRepository) != 0)
		}

		// TODO handle the DockerImage case
	}

	return flow, covered
}

// NewImagePipelineFromImageTagLocation returns the ImagePipeline and all the nodes contributing to it
func NewImagePipelineFromImageTagLocation(g osgraph.Graph, node graph.Node, imageTagLocation ImageTagLocation) (ImagePipeline, IntSet) {
	covered := IntSet{}
	covered.Insert(node.ID())

	flow := ImagePipeline{}
	flow.Image = imageTagLocation

	for _, input := range g.PredecessorNodesByEdgeKind(node, buildedges.BuildOutputEdgeKind) {
		covered.Insert(input.ID())
		build := input.(*buildgraph.BuildConfigNode)
		if flow.Build != nil {
			// FIXME: report this as an error (unexpected duplicate input build)
		}
		if build.BuildConfig == nil {
			// report this as as a missing build / broken link
			break
		}

		base, src, coveredInputs, scheduled, _ := findBuildInputs(g, build)
		covered.Insert(coveredInputs.List()...)
		flow.BaseImage = base
		flow.Source = src
		flow.Build = build
		flow.ScheduledImport = scheduled
		flow.LastSuccessfulBuild, flow.LastUnsuccessfulBuild, flow.ActiveBuilds = buildedges.RelevantBuilds(g, flow.Build)
	}

	for _, input := range g.SuccessorNodesByEdgeKind(node, imageedges.ReferencedImageStreamGraphEdgeKind) {
		covered.Insert(input.ID())
		imageStreamNode := input.(*imagegraph.ImageStreamNode)

		flow.DestinationResolved = (len(imageStreamNode.Status.DockerImageRepository) != 0)
	}
	for _, input := range g.SuccessorNodesByEdgeKind(node, imageedges.ReferencedImageStreamImageGraphEdgeKind) {
		covered.Insert(input.ID())
		imageStreamNode := input.(*imagegraph.ImageStreamNode)

		flow.DestinationResolved = (len(imageStreamNode.Status.DockerImageRepository) != 0)
	}

	return flow, covered
}

func findBuildInputs(g osgraph.Graph, bcNode *buildgraph.BuildConfigNode) (base ImageTagLocation, source SourceLocation, covered IntSet, scheduled bool, err error) {
	covered = IntSet{}

	// find inputs to the build
	for _, input := range g.PredecessorNodesByEdgeKind(bcNode, buildedges.BuildInputEdgeKind) {
		if source != nil {
			// report this as an error (unexpected duplicate source)
		}
		covered.Insert(input.ID())
		source = input.(SourceLocation)
	}
	for _, input := range g.PredecessorNodesByEdgeKind(bcNode, buildedges.BuildInputImageEdgeKind) {
		if base != nil {
			// report this as an error (unexpected duplicate input build)
		}
		covered.Insert(input.ID())
		base = input.(ImageTagLocation)
		scheduled = imageStreamTagScheduled(g, input, base)
	}

	return
}

func findBuildOutput(g osgraph.Graph, bcNode *buildgraph.BuildConfigNode) (result ImageTagLocation) {
	for _, output := range g.SuccessorNodesByEdgeKind(bcNode, buildedges.BuildOutputEdgeKind) {
		result = output.(ImageTagLocation)
		return
	}
	return
}

func imageStreamTagScheduled(g osgraph.Graph, input graph.Node, base ImageTagLocation) (scheduled bool) {
	for _, uncastImageStreamNode := range g.SuccessorNodesByEdgeKind(input, imageedges.ReferencedImageStreamGraphEdgeKind) {
		imageStreamNode := uncastImageStreamNode.(*imagegraph.ImageStreamNode)
		if imageStreamNode.ImageStream != nil {
			for _, tag := range imageStreamNode.ImageStream.Spec.Tags {
				if tag.Name == base.ImageTag() {
					scheduled = tag.ImportPolicy.Scheduled
					return
				}
			}
		}
	}
	return
}

type SortedImagePipelines []ImagePipeline

func (m SortedImagePipelines) Len() int      { return len(m) }
func (m SortedImagePipelines) Swap(i, j int) { m[i], m[j] = m[j], m[i] }
func (m SortedImagePipelines) Less(i, j int) bool {
	return CompareImagePipeline(&m[i], &m[j])
}

func CompareImagePipeline(a, b *ImagePipeline) bool {
	switch {
	case a.Build != nil && b.Build != nil && a.Build.BuildConfig != nil && b.Build.BuildConfig != nil:
		return CompareObjectMeta(&a.Build.BuildConfig.ObjectMeta, &b.Build.BuildConfig.ObjectMeta)
	case a.Build != nil && a.Build.BuildConfig != nil:
		return true
	case b.Build != nil && b.Build.BuildConfig != nil:
		return false
	}
	if a.Image == nil || b.Image == nil {
		return true
	}
	return a.Image.ImageSpec() < b.Image.ImageSpec()
}
