package main

import (
	"fmt"
	"io/ioutil"
	"net/url"
	"os"
	"path"

	"github.com/sirupsen/logrus"

	"github.com/osbuild/osbuild-composer/internal/common"
	"github.com/osbuild/osbuild-composer/internal/distro"
	"github.com/osbuild/osbuild-composer/internal/upload/koji"
	"github.com/osbuild/osbuild-composer/internal/worker"
	"github.com/osbuild/osbuild-composer/internal/worker/clienterrors"
)

type OSBuildKojiJobImpl struct {
	Store              string
	Output             string
	KojiServers        map[string]koji.GSSAPICredentials
	relaxTimeoutFactor uint
}

func (impl *OSBuildKojiJobImpl) kojiUpload(file *os.File, server, directory, filename string) (string, uint64, error) {
	transport := koji.CreateKojiTransport(impl.relaxTimeoutFactor)

	serverURL, err := url.Parse(server)
	if err != nil {
		return "", 0, err
	}

	creds, exists := impl.KojiServers[serverURL.Hostname()]
	if !exists {
		return "", 0, fmt.Errorf("Koji server has not been configured: %s", serverURL.Hostname())
	}

	k, err := koji.NewFromGSSAPI(server, &creds, transport)
	if err != nil {
		return "", 0, err
	}
	defer func() {
		err := k.Logout()
		if err != nil {
			logrus.Warnf("koji logout failed: %v", err)
		}
	}()

	return k.Upload(file, directory, filename)
}

func validateKojiResult(result *worker.OSBuildKojiJobResult, jobID string) {
	logWithId := logrus.WithField("jobId", jobID)
	if result.JobError != nil {
		logWithId.Errorf("osbuild job failed: %s", result.JobError.Reason)
		return
	}
	// if the job failed, but the JobError is
	// nil, we still need to handle this as an error
	if result.OSBuildOutput == nil || !result.OSBuildOutput.Success {
		reason := "osbuild job was unsuccessful"
		logWithId.Errorf("osbuild job failed: %s", reason)
		result.JobError = clienterrors.WorkerClientError(clienterrors.ErrorBuildJob, reason)
	} else {
		logWithId.Infof("osbuild-koji job succeeded")
	}
}

func (impl *OSBuildKojiJobImpl) Run(job worker.Job) error {
	var result worker.OSBuildKojiJobResult
	outputDirectory, err := ioutil.TempDir(impl.Output, job.Id().String()+"-*")
	if err != nil {
		return fmt.Errorf("error creating temporary output directory: %v", err)
	}
	defer func() {
		validateKojiResult(&result, job.Id().String())

		// this is necessary for early return errors
		err = job.Update(&result)
		if err != nil {
			logrus.Warnf("Error reporting job result: %v", err)
		}

		err := os.RemoveAll(outputDirectory)
		if err != nil {
			logrus.Warnf("Error removing temporary output directory (%s): %v", outputDirectory, err)
		}
	}()

	var args worker.OSBuildKojiJob
	err = job.Args(&args)
	if err != nil {
		return err
	}

	var initArgs worker.KojiInitJobResult
	err = job.DynamicArgs(0, &initArgs)
	if err != nil {
		return err
	}

	result.Arch = common.CurrentArch()
	result.HostOS, err = distro.GetRedHatRelease()
	if err != nil {
		return err
	}

	// In case the manifest is empty, try to get it from dynamic args
	if len(args.Manifest) == 0 {
		if job.NDynamicArgs() > 1 {
			var manifestJR worker.ManifestJobByIDResult
			err = job.DynamicArgs(1, &manifestJR)
			if err != nil {
				result.JobError = clienterrors.WorkerClientError(clienterrors.ErrorParsingDynamicArgs, "Error parsing dynamic args")
				return err
			}

			// skip the job if the manifest generation failed
			if manifestJR.JobError != nil {
				result.JobError = clienterrors.WorkerClientError(clienterrors.ErrorManifestDependency, "Manifest dependency failed")
				return nil
			}
			args.Manifest = manifestJR.Manifest
			if len(args.Manifest) == 0 {
				err := fmt.Errorf("Received empty manifest")
				result.JobError = clienterrors.WorkerClientError(clienterrors.ErrorManifestDependency, err.Error())
				return err
			}
		} else {
			err := fmt.Errorf("Job has no manifest")
			result.JobError = clienterrors.WorkerClientError(clienterrors.ErrorManifestDependency, err.Error())
			return err
		}
	}

	if initArgs.JobError == nil {
		exports := args.Exports
		if len(exports) == 0 {
			// job did not define exports, likely coming from an older version of composer
			// fall back to default "assembler"
			exports = []string{"assembler"}
		} else if len(exports) > 1 {
			// this worker only supports returning one (1) export
			err = fmt.Errorf("at most one build artifact can be exported")
			result.JobError = clienterrors.WorkerClientError(clienterrors.ErrorBuildJob, err.Error())
			return err
		}
		result.OSBuildOutput, err = RunOSBuild(args.Manifest, impl.Store, outputDirectory, exports, os.Stderr)
		if err != nil {
			return err
		}

		// NOTE: Currently OSBuild supports multiple exports, but this isn't used
		// by any of the image types and it can't be specified during the request.
		// Use the first (and presumably only) export for the imagePath.
		exportPath := exports[0]
		if result.OSBuildOutput.Success {
			f, err := os.Open(path.Join(outputDirectory, exportPath, args.ImageName))
			if err != nil {
				return err
			}
			result.ImageHash, result.ImageSize, err = impl.kojiUpload(f, args.KojiServer, args.KojiDirectory, args.KojiFilename)
			if err != nil {
				result.JobError = clienterrors.WorkerClientError(clienterrors.ErrorKojiBuild, err.Error())
			}
		}
	}

	// copy pipeline info to the result
	result.PipelineNames = args.PipelineNames

	return nil
}
