package app

import (
	"context"
	"fmt"
	"os"
	"os/user"
	"sort"
	"strings"

	"go.sour.is/pkg/mercury"
	"go.sour.is/pkg/ident"
	"go.sour.is/pkg/rsql"
	"go.sour.is/pkg/set"
)

const (
	mercurySource   = "mercury.source.*"
	mercuryPriority = "mercury.priority"
	mercuryHost     = "mercury.host"
	appDotEnviron   = "mercury.environ"
)
var (
	mercuryPolicy   = func(id string) string { return "mercury.@" + id + ".policy" }
)

func Register(name string, cfg mercury.SpaceMap) {
	for _, c := range cfg {
		c.Tags = append(c.Tags, "RO")
	}
	mercury.Registry.Register("mercury-default", func(s *mercury.Space) any { return &mercuryDefault{name: name, cfg: cfg} })
	mercury.Registry.Register("mercury-environ", func(s *mercury.Space) any { return &mercuryEnviron{cfg: cfg, lookup: mercury.Registry.GetRules} })
}

type hasRole interface {
	HasRole(r ...string) bool
}

type mercuryEnviron struct {
	cfg    mercury.SpaceMap
	lookup func(context.Context, ident.Ident) (mercury.Rules, error)
}

// Index returns nil
func (app *mercuryEnviron) GetIndex(ctx context.Context, search mercury.NamespaceSearch, _ *rsql.Program) (lis mercury.Config, err error) {

	if search.Match(mercurySource) {
		for _, s := range app.cfg.ToArray() {
			if search.Match(s.Space) {
				lis = append(lis, &mercury.Space{Space: s.Space, Tags: []string{"RO"}})
			}
		}
	}

	if search.Match(mercuryPriority) {
		lis = append(lis, &mercury.Space{Space: mercuryPriority, Tags: []string{"RO"}})
	}

	if search.Match(mercuryHost) {
		lis = append(lis, &mercury.Space{Space: mercuryHost, Tags: []string{"RO"}})
	}

	if search.Match(appDotEnviron) {
		lis = append(lis, &mercury.Space{Space: appDotEnviron, Tags: []string{"RO"}})
	}
	if id := ident.FromContext(ctx); id != nil {
		identity := id.Identity()
		match := mercuryPolicy(identity)
		if search.Match(match) {
			lis = append(lis, &mercury.Space{Space: match, Tags: []string{"RO"}})
		}
	}
	return
}

// Objects returns nil
func (app *mercuryEnviron) GetConfig(ctx context.Context, search mercury.NamespaceSearch, _ *rsql.Program, _ []string) (lis mercury.Config, err error) {
	if search.Match(mercurySource) {
		for _, s := range app.cfg.ToArray() {
			if search.Match(s.Space) {
				lis = append(lis, s)
			}
		}
	}

	if search.Match(mercuryPriority) {
		space := mercury.Space{
			Space: mercuryPriority,
			Tags:  []string{"RO"},
		}

		// for i, key := range mercury.Registry {
		// 	space.List = append(space.List, mercury.Value{
		// 		Space:  appDotPriority,
		// 		Seq:    uint64(i),
		// 		Name:   key.Match,
		// 		Values: []string{fmt.Sprint(key.Priority)},
		// 	})
		// }

		lis = append(lis, &space)
	}

	if search.Match(mercuryHost) {
		if usr, err := user.Current(); err == nil {
			space := mercury.Space{
				Space: mercuryHost,
				Tags:  []string{"RO"},
			}

			hostname, _ := os.Hostname()
			wd, _ := os.Getwd()
			grp, _ := usr.GroupIds()
			space.List = []mercury.Value{
				{
					Space:  mercuryHost,
					Seq:    1,
					Name:   "hostname",
					Values: []string{hostname},
				},
				{
					Space:  mercuryHost,
					Seq:    2,
					Name:   "username",
					Values: []string{usr.Username},
				},
				{
					Space:  mercuryHost,
					Seq:    3,
					Name:   "uid",
					Values: []string{usr.Uid},
				},
				{
					Space:  mercuryHost,
					Seq:    4,
					Name:   "gid",
					Values: []string{usr.Gid},
				},
				{
					Space:  mercuryHost,
					Seq:    5,
					Name:   "display",
					Values: []string{usr.Name},
				},
				{
					Space:  mercuryHost,
					Seq:    6,
					Name:   "home",
					Values: []string{usr.HomeDir},
				},
				{
					Space:  mercuryHost,
					Seq:    7,
					Name:   "groups",
					Values: grp,
				},
				{
					Space:  mercuryHost,
					Seq:    8,
					Name:   "pid",
					Values: []string{fmt.Sprintf("%v", os.Getpid())},
				},
				{
					Space:  mercuryHost,
					Seq:    9,
					Name:   "wd",
					Values: []string{wd},
				},
			}

			lis = append(lis, &space)
		}
	}

	if search.Match(appDotEnviron) {
		env := os.Environ()
		space := mercury.Space{
			Space: appDotEnviron,
			Tags:  []string{"RO"},
		}

		sort.Strings(env)
		for i, s := range env {
			key, val, _ := strings.Cut(s, "=")

			vals := []string{val}
			if strings.Contains(key, "PATH") || strings.Contains(key, "XDG") {
				vals = strings.Split(val, ":")
			}

			space.List = append(space.List, mercury.Value{
				Space:  appDotEnviron,
				Seq:    uint64(i),
				Name:   key,
				Values: vals,
			})
		}
		lis = append(lis, &space)
	}

	if id := ident.FromContext(ctx); id != nil {
		identity := id.Identity()
		groups := groups(identity, &app.cfg)
		match := mercuryPolicy(identity)
		if search.Match(match) {
			space := &mercury.Space{
				Space: match,
				Tags:  []string{"RO"},
			}

			lis = append(lis, space)
			rules, err := app.lookup(ctx, id)
			if err != nil {
				space.AddNotes(err.Error())
			} else {
				k := mercury.NewValue("groups")
				k.AddValues(strings.Join(groups.Values(), " "))
				space.AddKeys(k)

				k = mercury.NewValue("rules")
				for _, r := range rules {
					k.AddValues(strings.Join([]string{r.Role, r.Type, r.Match}, " "))
				}
				space.AddKeys(k)
			}
		}
	}

	return
}

// Rules returns nil
func (app *mercuryEnviron) GetRules(ctx context.Context, id ident.Ident) (lis mercury.Rules, err error) {
	identity := id.Identity()

	lis = append(lis,
		mercury.Rule{
			Role:  "read",
			Type:  "NS",
			Match: mercuryPolicy(identity),
		},
	)

	groups := groups(identity, &app.cfg)

	if u, ok := id.(hasRole); groups.Has("admin") || ok && u.HasRole("admin") {
		lis = append(lis,
			mercury.Rule{
				Role:  "read",
				Type:  "NS",
				Match: mercurySource,
			},
			mercury.Rule{
				Role:  "read",
				Type:  "NS",
				Match: mercuryPriority,
			},
			mercury.Rule{
				Role:  "read",
				Type:  "NS",
				Match: mercuryHost,
			},
			mercury.Rule{
				Role:  "read",
				Type:  "NS",
				Match: appDotEnviron,
			},
		)
	}

	return lis, nil
}

func groups(identity string, cfg *mercury.SpaceMap) set.Set[string] {
	groups := set.New[string]()
	if s, ok := cfg.Space("mercury.groups"); ok {
		for _, g := range s.List {
			for _, v := range g.Values {
				for _, u := range strings.Fields(v) {
					if u == identity {
						groups.Add(g.Name)
					}
				}
			}
		}
	}
	return groups
}
