// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.

package resource

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"gopkg.in/yaml.v3"
	"io"
	"log/slog"
	"net/url"
	_ "os"
	_ "os/exec"
	"path/filepath"
	"strings"
	"gitea.rosskeen.house/rosskeen.house/machine"
)

type PackageType string

const (
	PackageTypeApk PackageType = "apk"
	PackageTypeApt PackageType = "apt"
	PackageTypeDeb PackageType = "deb"
	PackageTypeDnf PackageType = "dnf"
	PackageTypeRpm PackageType = "rpm"
	PackageTypePip PackageType = "pip"
	PackageTypeYum PackageType = "yum"
)

type Package struct {
	Name        string      `yaml:"name" json:"name"`
	Required    string      `json:"required" yaml:"required"`
	Version     string      `yaml:"version" json:"version"`
	PackageType PackageType `yaml:"type" json:"type"`

	CreateCommand *Command `yaml:"-" json:"-"`
	ReadCommand   *Command `yaml:"-" json:"-"`
	UpdateCommand *Command `yaml:"-" json:"-"`
	DeleteCommand *Command `yaml:"-" json:"-"`
	// state attributes
	State string `yaml:"state" json:"state"`
}

func init() {
	ResourceTypes.Register("package", func(u *url.URL) Resource {
		p := NewPackage()
		return p
	})
	ResourceTypes.Register(string(PackageTypeApk), func(u *url.URL) Resource {
		p := NewPackage()
		return p
	})
	ResourceTypes.Register(string(PackageTypeApt), func(u *url.URL) Resource {
		p := NewPackage()
		return p
	})
	ResourceTypes.Register(string(PackageTypeDeb), func(u *url.URL) Resource {
		p := NewPackage()
		return p
	})
	ResourceTypes.Register(string(PackageTypeDnf), func(u *url.URL) Resource {
		p := NewPackage()
		return p
	})
	ResourceTypes.Register(string(PackageTypeRpm), func(u *url.URL) Resource {
		p := NewPackage()
		return p
	})
	ResourceTypes.Register(string(PackageTypePip), func(u *url.URL) Resource {
		p := NewPackage()
		return p
	})
	ResourceTypes.Register(string(PackageTypeYum), func(u *url.URL) Resource {
		p := NewPackage()
		return p
	})
}

func NewPackage() *Package {
	return &Package{}
}

func (p *Package) Clone() Resource {
	newp := &Package {
		Name: p.Name,
		Required: p.Required,
		Version: p.Version,
		PackageType: p.PackageType,
		State: p.State,
	}
	newp.CreateCommand, newp.ReadCommand, newp.UpdateCommand, newp.DeleteCommand = newp.PackageType.NewCRUD()
	return newp
}

func (p *Package) StateMachine() machine.Stater {
	return StorageMachine()
}

func (p *Package) URI() string {
	return fmt.Sprintf("package://%s?version=%s&type=%s", p.Name, p.Version, p.PackageType)
}

func (p *Package) SetURI(uri string) error {
	resourceUri, e := url.Parse(uri)
	if e == nil {
		if resourceUri.Scheme == "package" {
			p.Name = filepath.Join(resourceUri.Hostname(), resourceUri.Path)
			p.Version = resourceUri.Query().Get("version")
			if p.Version == "" {
				p.Version = "latest"
			}
			p.PackageType = PackageType(resourceUri.Query().Get("type"))
			if p.PackageType == "" {
				e = fmt.Errorf("%w: %s is not a package known resource ", ErrInvalidResourceURI, uri)
			}
		} else {
			e = fmt.Errorf("%w: %s is not a package resource ", ErrInvalidResourceURI, uri)
		}
	}
	return e
}

func (p *Package) JSON() ([]byte, error) {
	return json.Marshal(p)
}

func (p *Package) Validate() error {
	s := NewSchema(p.Type())
	jsonDoc, jsonErr := p.JSON()
	if jsonErr == nil {
		return s.Validate(string(jsonDoc))
	}
	return jsonErr
}

func (p *Package) ResolveId(ctx context.Context) string {
	return ""
}

func (p *Package) Apply() error {
	if p.Version == "latest" {
		p.Version = ""
	}
	_, err := p.CreateCommand.Execute(p)
	if err != nil {
		return err
	}

	_,e := p.Read(context.Background())
	return e
}

func (p *Package) Load(r io.Reader) error {
	c := NewYAMLDecoder(r)
	return c.Decode(p)
}

func (p *Package) LoadDecl(yamlResourceDeclaration string) error {
	c := NewYAMLStringDecoder(yamlResourceDeclaration)
	return c.Decode(p)
}

func (p *Package) Type() string { return "package" }

func (p *Package) Read(ctx context.Context) ([]byte, error) {
	out, err := p.ReadCommand.Execute(p)
	if err != nil {
		return nil, err
	}
	exErr := p.ReadCommand.Extractor(out, p)
	if exErr != nil {
		return nil, exErr
	}
	return yaml.Marshal(p)
}

func (p *Package) UnmarshalJSON(data []byte) error {
	if unmarshalErr := json.Unmarshal(data, p); unmarshalErr != nil {
		return unmarshalErr
	}
	p.CreateCommand, p.ReadCommand, p.UpdateCommand, p.DeleteCommand = p.PackageType.NewCRUD()
	return nil
}

func (p *Package) UnmarshalYAML(value *yaml.Node) error {
	type decodePackage Package
	if unmarshalErr := value.Decode((*decodePackage)(p)); unmarshalErr != nil {
		return unmarshalErr
	}
	p.CreateCommand, p.ReadCommand, p.UpdateCommand, p.DeleteCommand = p.PackageType.NewCRUD()
	return nil
}

func (p *PackageType) NewCRUD() (create *Command, read *Command, update *Command, del *Command) {
	switch *p {
	case PackageTypeApk:
		return NewApkCreateCommand(), NewApkReadCommand(), NewApkUpdateCommand(), NewApkDeleteCommand()
	case PackageTypeApt:
		return NewAptCreateCommand(), NewAptReadCommand(), NewAptUpdateCommand(), NewAptDeleteCommand()
	case PackageTypeDeb:
	case PackageTypeDnf:
	case PackageTypeRpm:
	case PackageTypePip:
	case PackageTypeYum:
	default:
	}
	return nil, nil, nil, nil
}

func (p *PackageType) UnmarshalValue(value string) error {
	switch value {
	case string(PackageTypeApk), string(PackageTypeApt), string(PackageTypeDeb), string(PackageTypeDnf), string(PackageTypeRpm), string(PackageTypePip), string(PackageTypeYum):
		*p = PackageType(value)
		return nil
	default:
		return errors.New("invalid PackageType value")
	}
}

func (p *PackageType) UnmarshalJSON(data []byte) error {
	var s string
	if unmarshalRouteTypeErr := json.Unmarshal(data, &s); unmarshalRouteTypeErr != nil {
		return unmarshalRouteTypeErr
	}
	return p.UnmarshalValue(s)
}

func (p *PackageType) UnmarshalYAML(value *yaml.Node) error {
	var s string
	if err := value.Decode(&s); err != nil {
		return err
	}
	return p.UnmarshalValue(s)
}

func NewApkCreateCommand() *Command {
	c := NewCommand()
	c.Path = "apk"
	c.Args = []CommandArg{
		CommandArg("add"),
		CommandArg("{{ .Name }}{{ .Required }}"),
	}
	return c
}

func NewApkReadCommand() *Command {
	c := NewCommand()
	c.Path = "apk"
	c.Args = []CommandArg{
		CommandArg("info"),
		CommandArg("-ev"),
		CommandArg("{{ .Name }}"),
	}
	c.Extractor = func(out []byte, target any) error {
		p := target.(*Package)
		pkg := strings.Split(string(out), "-")
		if pkg[0] == p.Name {
			p.Name = pkg[0]
			p.Version = pkg[1]
			p.State = "present"
		} else {
			p.State = "absent"
		}
		return nil
	}
	return c
}

func NewApkUpdateCommand() *Command {
	c := NewCommand()
	c.Path = "apk"
	c.Args = []CommandArg{
		CommandArg("del"),
		CommandArg("{{ .Name }}"),
	}
	return c
}

func NewApkDeleteCommand() *Command {
	c := NewCommand()
	c.Path = "apk"
	c.Args = []CommandArg{
		CommandArg("del"),
		CommandArg("{{ .Name }}"),
	}
	return c
}

func NewAptCreateCommand() *Command {
	c := NewCommand()
	c.Path = "apt-get"
	c.Args = []CommandArg{
		CommandArg("satisfy"),
		CommandArg("-y"),
		CommandArg("{{ .Name }} ({{ .Required }})"),
	}
	return c
}

func NewAptReadCommand() *Command {
	c := NewCommand()
	c.Path = "dpkg"
	c.Args = []CommandArg{
		CommandArg("-s"),
		CommandArg("{{ .Name }}"),
	}
	c.Extractor = func(out []byte, target any) error {
		p := target.(*Package)
		slog.Info("Extract()", "out", out)
		pkginfo := strings.Split(string(out), "\n")
		for _, infofield := range pkginfo {
			if len(infofield) > 0 && infofield[0] != ' ' {
				fieldKeyValue := strings.SplitN(infofield, ":", 2)
				if len(fieldKeyValue) > 1 {
					key := strings.TrimSpace(fieldKeyValue[0])
					value := strings.TrimSpace(fieldKeyValue[1])
					switch key {
					case "Package":
						if value != p.Name {
							p.State = "absent"
							return nil
						}
					case "Status":
						statusFields := strings.SplitN(value, " ", 3)
						if len(statusFields) > 1 {
							if statusFields[2] == "installed" {
								p.State = "present"
							} else {
								p.State = "absent"
							}
						}
					case "Version":
						p.Version = value
					}
				}
			}
		}
		slog.Info("Extract()", "package", p)
		return nil
	}
	return c
}

func NewAptUpdateCommand() *Command {
	c := NewCommand()
	c.Path = "apt"
	c.Args = []CommandArg{
		CommandArg("install"),
		CommandArg("{{ .Name }}"),
	}
	return c
}

func NewAptDeleteCommand() *Command {
	c := NewCommand()
	c.Path = "apt"
	c.Args = []CommandArg{
		CommandArg("remove"),
		CommandArg("{{ .Name }}"),
	}
	return c
}
