package build

import (
	"fmt"
	"strings"
	"time"

	kubeapi "github.com/GoogleCloudPlatform/kubernetes/pkg/api"
	kubeclient "github.com/GoogleCloudPlatform/kubernetes/pkg/client"
	"github.com/GoogleCloudPlatform/kubernetes/pkg/labels"
	"github.com/GoogleCloudPlatform/kubernetes/pkg/util"

	"github.com/golang/glog"
	"github.com/openshift/origin/pkg/build/api"
	osclient "github.com/openshift/origin/pkg/client"
)

// BuildJobStrategy represents a strategy for executing a build by
// creating a pod definition that will execute the build
type BuildJobStrategy interface {
	CreateBuildPod(build *api.Build) (*kubeapi.Pod, error)
}

// BuildController watches build resources and manages their state
type BuildController struct {
	osClient        osclient.Interface
	kubeClient      kubeclient.Interface
	buildStrategies map[api.BuildType]BuildJobStrategy
	timeout         int
}

// NewBuildController creates a new build controller
func NewBuildController(kc kubeclient.Interface,
	oc osclient.Interface,
	strategies map[api.BuildType]BuildJobStrategy,
	timeout int) *BuildController {

	glog.Infof("Creating build controller with timeout=%d", timeout)

	bc := &BuildController{
		kubeClient:      kc,
		osClient:        oc,
		buildStrategies: strategies,
		timeout:         timeout,
	}
	return bc

}

// Run begins watching and syncing build jobs onto the cluster.
func (bc *BuildController) Run(period time.Duration) {
	syncTime := time.Tick(period)
	go util.Forever(func() { bc.watchBuilds(syncTime) }, period)
}

// The main sync loop. Iterates over current builds and delegates syncing.
func (bc *BuildController) watchBuilds(syncTime <-chan time.Time) {
	for {
		select {
		case <-syncTime:
			builds, err := bc.osClient.ListBuilds(labels.Everything())
			if err != nil {
				glog.Errorf("Error listing builds: %v (%#v)", err, err)
				return
			}
			for _, build := range builds.Items {
				nextStatus, err := bc.synchronize(&build)
				if err != nil {
					glog.Errorf("Error synchronizing build ID %v: %#v", build.ID, err)
				}

				if nextStatus != build.Status {
					build.Status = nextStatus
					if _, err := bc.osClient.UpdateBuild(&build); err != nil {
						glog.Errorf("Error updating build ID %v to status %v: %#v", build.ID, nextStatus, err)
					}
				}
			}
		}

	}
}

func hasTimeoutElapsed(build *api.Build, timeout int) bool {
	timestamp := build.CreationTimestamp
	elapsed := time.Since(timestamp.Time)
	return int(elapsed.Seconds()) > timeout
}

// Determine the next status of a build given its current state and the state
// of its associated pod.
// TODO: improve handling of illegal state transitions
func (bc *BuildController) synchronize(build *api.Build) (api.BuildStatus, error) {
	glog.Infof("Syncing build %s", build.ID)

	switch build.Status {
	case api.BuildNew:
		build.PodID = "build-" + string(build.Input.Type) + "-" + build.ID // TODO: better naming
		return api.BuildPending, nil
	case api.BuildPending:
		buildStrategy, ok := bc.buildStrategies[build.Input.Type]
		if !ok {
			return api.BuildError, fmt.Errorf("No build type for %s", build.Input.Type)
		}

		podSpec, err := buildStrategy.CreateBuildPod(build)
		if err != nil {
			glog.Errorf("Unable to create build pod: %v", err)
			return api.BuildFailed, err
		}

		glog.Infof("Attempting to create pod: %#v", podSpec)
		_, err = bc.kubeClient.CreatePod(podSpec)

		// TODO: strongly typed error checking
		if err != nil {
			if strings.Index(err.Error(), "already exists") != -1 {
				return build.Status, err // no transition, already handled by someone else
			}

			return api.BuildFailed, err
		}

		return api.BuildRunning, nil
	case api.BuildRunning:
		if timedOut := hasTimeoutElapsed(build, bc.timeout); timedOut {
			return api.BuildFailed, fmt.Errorf("Build timed out")
		}

		pod, err := bc.kubeClient.GetPod(build.PodID)
		if err != nil {
			return build.Status, fmt.Errorf("Error retrieving pod for build ID %v: %#v", build.ID, err)
		}

		// pod is still running
		if pod.CurrentState.Status != kubeapi.PodTerminated {
			return build.Status, nil
		}

		var nextStatus = api.BuildComplete

		// check the exit codes of all the containers in the pod
		for _, info := range pod.CurrentState.Info {
			if info.State.ExitCode != 0 {
				nextStatus = api.BuildFailed
			}
		}
		return nextStatus, nil
	case api.BuildComplete, api.BuildFailed, api.BuildError:
		return build.Status, nil
	default:
		return api.BuildError, fmt.Errorf("Invalid build status: %s", build.Status)
	}
}