package common

import (
	"bytes"
	"encoding/xml"
	"fmt"
	"io"
	"log"
	"net/http"
	"net/url"
	"slices"
)

type ObsClient struct {
	baseUrl        *url.URL
	client         *http.Client
	user, password string
	cookie         string

	HomeProject string
}

func NewObsClient(host string) (*ObsClient, error) {
	baseUrl, err := url.Parse("https://" + host)
	if err != nil {
		return nil, err
	}

	return &ObsClient{
		baseUrl:  baseUrl,
		client:   &http.Client{},
		user:     obsUser,
		password: obsPassword,

		HomeProject: fmt.Sprintf("home:%s", obsUser),
	}, nil
}

type RepositoryPathMeta struct {
	Project    string `xml:"project,attr"`
	Repository string `xml:"repository,attr"`
}

type RepositoryMeta struct {
	Name         string               `xml:"name,attr"`
	BuildTrigger string               `xml:"rebuild,attr"`
	BlockMode    string               `xml:"block"`
	LinkedBuild  string               `xml:"linkedbuild"`
	Archs        []string             `xml:"arch"`
	Paths        []RepositoryPathMeta `xml:"path"`
}

type Flags struct {
	Contents string `xml:",innerxml"`
}

type ProjectMeta struct {
	XMLName      xml.Name         `xml:"project"`
	Name         string           `xml:"name,attr"`
	Title        string           `xml:"title"`
	Description  string           `xml:"description"`
	Url          string           `xml:"url"`
	ScmSync      string           `xml:"scmsync"`
	Repositories []RepositoryMeta `xml:"repository"`

	BuildFlags  Flags `xml:"build"`
	PublicFlags Flags `xml:"publish"`
	DebugFlags  Flags `xml:"debuginfo"`
	UseForBuild Flags `xml:"useforbuild"`
}

func parseProjectMeta(data []byte) (*ProjectMeta, error) {
	var meta ProjectMeta
	err := xml.Unmarshal(data, &meta)
	if err != nil {
		return nil, err
	}

	return &meta, nil
}

func (c *ObsClient) GetProjectMeta(project string) (*ProjectMeta, error) {
	req, err := http.NewRequest("GET", c.baseUrl.JoinPath("source", project, "_meta").String(), nil)
	if err != nil {
		return nil, err
	}
	req.SetBasicAuth(c.user, c.password)
	log.Printf("request: %#v", *req.URL)
	log.Printf("headers: %#v", req.Header)
	res, err := c.client.Do(req)

	if err != nil {
		return nil, err
	}

	switch res.StatusCode {
	case 200:
		break
	case 404:
		return nil, nil
	default:
		return nil, fmt.Errorf("Unexpected return code: %d", res.StatusCode)
	}

	data, err := io.ReadAll(res.Body)
	if err != nil {
		return nil, err
	}

	return parseProjectMeta(data)
}

func ObsSafeProjectName(prjname string) string {
	if len(prjname) < 1 {
		return prjname
	} else if len(prjname) > 200 {
		prjname = prjname[:199]
	}

	switch prjname[0] {
	case '_', '.', ':':
		prjname = "X" + prjname[1:]
		// no UTF-8 in OBS :(
		//		prjname = "＿" + prjname[1:]
		//	case ':':
		//		prjname = "：" + prjname[1:]
		//	case '.':
		//		prjname = "․" + prjname[1:]
	}

	return prjname
}

var ValidBlockModes []string
var ValidPrjLinkModes []string
var ValidTriggerModes []string

func (c *ObsClient) SetProjectMeta(meta *ProjectMeta) error {

	for _, repo := range meta.Repositories {
		if len(repo.BlockMode) > 0 && !slices.Contains(ValidBlockModes, repo.BlockMode) {
			return fmt.Errorf("Invalid repository block mode: '%s'", repo.BlockMode)
		}
		if len(repo.BuildTrigger) > 0 && !slices.Contains(ValidTriggerModes, repo.BuildTrigger) {
			return fmt.Errorf("Invalid repository trigger mode: '%s'", repo.BuildTrigger)
		}
		if len(repo.LinkedBuild) > 0 && !slices.Contains(ValidPrjLinkModes, repo.LinkedBuild) {
			return fmt.Errorf("Invalid linked project rebuild mode: '%s'", repo.LinkedBuild)
		}
	}

	req, err := http.NewRequest("PUT", c.baseUrl.JoinPath("source", meta.Name, "_meta").String(), nil)
	if err != nil {
		return err
	}
	req.SetBasicAuth(c.user, c.password)
	xml, err := xml.Marshal(meta)
	if err != nil {
		return err
	}
	req.Body = io.NopCloser(bytes.NewReader(xml))
	log.Printf("headers: %#v", req.Header)
	log.Printf("xml: %s", xml)
	res, err := c.client.Do(req)

	if err != nil {
		return err
	}

	switch res.StatusCode {
	case 200:
		break
	default:
		return fmt.Errorf("Unexpected return code: %d", res.StatusCode)
	}

	return nil
}

func (c *ObsClient) DeleteProject(project string) error {
	req, err := http.NewRequest("DELETE", c.baseUrl.JoinPath("source", project).String(), nil)
	if err != nil {
		return err
	}

	req.SetBasicAuth(c.user, c.password)
	res, err := c.client.Do(req)

	if err != nil {
		return err
	}

	if res.StatusCode != 200 {
		return fmt.Errorf("Unexpected return code: %d", res.StatusCode)
	}

	return nil

}

type PackageBuildStatus struct {
	Package string `xml:"package,attr"`
	Code    string `xml:"code,attr"`
	Details string `xml:"details"`
}

type BuildResult struct {
	Project    string               `xml:"project,attr"`
	Repository string               `xml:"repository,attr"`
	Arch       string               `xml:"arch,attr"`
	Code       string               `xml:"code,attr"`
	Dirty      bool                 `xml:"dirty,attr"`
	Status     []PackageBuildStatus `xml:"status"`
	Binaries   []BinaryList         `xml:"binarylist"`
}

type Binary struct {
	Size     uint64 `xml:"size,attr"`
	Filename string `xml:"filename,attr"`
	Mtime    uint64 `xml:"mtime,attr"`
}

type BinaryList struct {
	Package string   `xml:"package,attr"`
	Binary  []Binary `xml:"binary"`
}

type BuildResultList struct {
	XMLName xml.Name      `xml:"resultlist"`
	Result  []BuildResult `xml:"result"`
}

func (r *BuildResultList) GetPackageList() []string {
	pkgList := make([]string, 0, 16)

	for _, res := range r.Result {
		// TODO: enough to iterate over one result set?

		for _, status := range res.Status {
			if !slices.Contains(pkgList, status.Package) {
				pkgList = append(pkgList, status.Package)
			}
		}
	}

	return pkgList
}

func (r *BuildResultList) BuildResultSummary() (success, finished bool) {
	if r == nil {
		return true, true
	}

	finished = len(r.Result) > 0 && len(r.Result[0].Status) > 0
	success = finished

	for _, resultSet := range r.Result {
		repoDetail, ok := ObsRepoStatusDetails[resultSet.Code]

		if !ok {
			panic("Unknown repo result code: " + resultSet.Code)
		}

		finished = repoDetail.Finished
		if !finished || resultSet.Dirty {
			return
		}

		for _, result := range resultSet.Status {
			detail, ok := ObsBuildStatusDetails[result.Code]

			if !ok {
				panic("Unknown result code: " + result.Code)
			}

			finished = finished && detail.Finished
			success = success && detail.Success

			if !finished {
				return
			}
		}
	}

	return
}

var ObsBuildStatusDetails map[string]ObsBuildStatusDetail
var ObsRepoStatusDetails map[string]ObsBuildStatusDetail

type ObsBuildStatusDetail struct {
	Code        string
	Description string
	Finished    bool
	Success     bool
}

func init() {
	ValidTriggerModes = []string{"transitive", "direct", "local"}
	ValidBlockModes = []string{"all", "local", "never"}
	ValidPrjLinkModes = []string{"off", "localdep", "alldirect", "all"}

	ObsBuildStatusDetails = make(map[string]ObsBuildStatusDetail)
	ObsRepoStatusDetails = make(map[string]ObsBuildStatusDetail)

	// package status
	ObsBuildStatusDetails["succeeded"] = ObsBuildStatusDetail{
		Code:        "succeeded",
		Description: "Package has built successfully and can be used to build further packages.",
		Finished:    true,
		Success:     true,
	}
	ObsBuildStatusDetails["failed"] = ObsBuildStatusDetail{
		Code:        "failed",
		Description: "The package does not build successfully. No packages have been created. Packages that depend on this package will be built using any previously created packages, if they exist.",
		Finished:    true,
		Success:     false,
	}
	ObsBuildStatusDetails["unresolvable"] = ObsBuildStatusDetail{
		Code:        "unresolvable",
		Description: "The build can not begin, because required packages are either missing or not explicitly defined.",
		Finished:    true,
		Success:     false,
	}
	ObsBuildStatusDetails["broken"] = ObsBuildStatusDetail{
		Code:        "broken",
		Description: "The sources either contain no build description (e.g. specfile), automatic source processing failed or a merge conflict does exist.",
		Finished:    true,
		Success:     false,
	}
	ObsBuildStatusDetails["blocked"] = ObsBuildStatusDetail{
		Code:        "blocked",
		Description: "This package waits for other packages to be built. These can be in the same or other projects.",
		Finished:    false,
	}
	ObsBuildStatusDetails["scheduled"] = ObsBuildStatusDetail{
		Code:        "scheduled",
		Description: "A package has been marked for building, but the build has not started yet.",
		Finished:    false,
	}
	ObsBuildStatusDetails["dispatching"] = ObsBuildStatusDetail{
		Code:        "dispatching",
		Description: "A package is being copied to a build host. This is an intermediate state before building.",
		Finished:    false,
	}
	ObsBuildStatusDetails["building"] = ObsBuildStatusDetail{
		Code:        "building",
		Description: "The package is currently being built.",
		Finished:    false,
	}
	ObsBuildStatusDetails["signing"] = ObsBuildStatusDetail{
		Code:        "signing",
		Description: "The package has been built successfully and is assigned to get signed.",
		Finished:    false,
	}
	ObsBuildStatusDetails["finished"] = ObsBuildStatusDetail{
		Code:        "finished",
		Description: "The package has been built and signed, but has not yet been picked up by the scheduler. This is an intermediate state prior to 'succeeded' or 'failed'.",
		Finished:    false,
	}
	ObsBuildStatusDetails["disabled"] = ObsBuildStatusDetail{
		Code:        "disabled",
		Description: "The package has been disabled from building in project or package metadata. Packages that depend on this package will be built using any previously created packages, if they still exist.",
		Finished:    true,
		Success:     true,
	}
	ObsBuildStatusDetails["excluded"] = ObsBuildStatusDetail{
		Code:        "excluded",
		Description: "The package build has been disabled in package build description (for example in the .spec file) or does not provide a matching build description for the target.",
		Finished:    true,
		Success:     true,
	}
	ObsBuildStatusDetails["locked"] = ObsBuildStatusDetail{
		Code:        "locked",
		Description: "The package is frozen",
		Finished:    true,
		Success:     true,
	}
	ObsBuildStatusDetails["unknown"] = ObsBuildStatusDetail{
		Code:        "unknown",
		Description: "The scheduler has not yet evaluated this package. Should be a short intermediate state for new packages.",
		Finished:    false,
	}

	// repo status
	ObsRepoStatusDetails["published"] = ObsBuildStatusDetail{
		Code:        "published",
		Description: "Repository has been published",
		Finished:    true,
	}
	ObsRepoStatusDetails["publishing"] = ObsBuildStatusDetail{
		Code:        "publishing",
		Description: "Repository is being created right now",
		Finished:    true,
	}
	ObsRepoStatusDetails["unpublished"] = ObsBuildStatusDetail{
		Code:        "unpublished",
		Description: "Build finished, but repository publishing is disabled",
		Finished:    true,
	}
	ObsRepoStatusDetails["building"] = ObsBuildStatusDetail{
		Code:        "building",
		Description: "Build jobs exist for the repository",
		Finished:    false,
	}
	ObsRepoStatusDetails["finished"] = ObsBuildStatusDetail{
		Code:        "finished",
		Description: "Build jobs have been processed, new repository is not yet created",
		Finished:    true,
	}
	ObsRepoStatusDetails["blocked"] = ObsBuildStatusDetail{
		Code:        "blocked",
		Description: "No build possible at the moment, waiting for jobs in other repositories",
		Finished:    false,
	}
	ObsRepoStatusDetails["broken"] = ObsBuildStatusDetail{
		Code:        "broken",
		Description: "The repository setup is broken, build or publish not possible",
		Finished:    true,
	}
	ObsRepoStatusDetails["scheduling"] = ObsBuildStatusDetail{
		Code:        "scheduling",
		Description: "The repository state is being calculated right now",
		Finished:    false,
	}
}

func parseBuildResults(data []byte) (*BuildResultList, error) {
	result := BuildResultList{}
	err := xml.Unmarshal(data, &result)
	if err != nil {
		return nil, err
	}

	return &result, nil
}

type ObsProjectNotFound struct {
	Project string
}

func (obs ObsProjectNotFound) Error() string {
	return fmt.Sprintf("OBS project is not found: %s", obs.Project)
}

func (c *ObsClient) BuildStatus(project string, packages ...string) (*BuildResultList, error) {
	u := c.baseUrl.JoinPath("build", project, "_result")
	query := u.Query()
	query.Add("view", "status")
	query.Add("view", "binarylist")
	query.Add("multibuild", "1")
	if len(packages) > 0 {
		query.Add("lastbuild", "1")
		for _, pkg := range packages {
			query.Add("package", pkg)
		}
	}
	u.RawQuery = query.Encode()
	req, err := http.NewRequest("GET", u.String(), nil)
	log.Print(u.String())
	if err != nil {
		return nil, err
	}

	req.SetBasicAuth(c.user, c.password)
	res, err := c.client.Do(req)

	if err != nil {
		return nil, err
	}

	switch res.StatusCode {
	case 200:
		break
	case 404:
		return nil, ObsProjectNotFound{project}
	default:
		return nil, fmt.Errorf("Unexpected return code: %d", res.StatusCode)
	}

	data, err := io.ReadAll(res.Body)
	if err != nil {
		return nil, err
	}
	return parseBuildResults(data)
}
