package knotacl

import (
	"context"
	"errors"
	"log/slog"
	"slices"
	"sync"
	"time"

	"golang.org/x/sync/errgroup"

	"tangled.org/core/appview/db"
	"tangled.org/core/appview/knotcompat"
	"tangled.org/core/appview/models"
	"tangled.org/core/appview/pages"
	"tangled.org/core/appview/pages/repoinfo"
	"tangled.org/core/consts"
	"tangled.org/core/rbac"
)

var ErrKnotUnreachable = errors.New("knot unreachable")

const (
	pickerFanoutBudget      = 3 * time.Second
	pickerFanoutConcurrency = 16
)

type Service struct {
	dev bool
	log *slog.Logger
	leg *legacyReader
	nat *nativeReader
}

func NewService(enforcer *rbac.Enforcer, execer db.Execer, dev bool, logger *slog.Logger) *Service {
	return &Service{
		dev: dev,
		log: logger,
		leg: &legacyReader{enforcer: enforcer},
		nat: &nativeReader{client: newCache(NewClient(dev, logger), cacheTTL, nil), execer: execer},
	}
}

func (s *Service) reader(ctx context.Context, host string) reader {
	if knotcompat.KnotHasCapability(ctx, host, s.dev, consts.CapKnotACL) {
		return s.nat
	}
	return s.leg
}

func (s *Service) RolesInRepo(ctx context.Context, repo *models.Repo, userDid string) repoinfo.RolesInRepo {
	return repoinfo.RolesInRepo{Roles: s.repoPerms(ctx, repo, userDid)}
}

func (s *Service) HasRepoPermission(ctx context.Context, repo *models.Repo, userDid, perm string) bool {
	return slices.Contains(s.repoPerms(ctx, repo, userDid), perm)
}

func (s *Service) HasRepoPermissionErr(ctx context.Context, repo *models.Repo, userDid, perm string) (bool, error) {
	perms, err := s.repoPermsErr(ctx, repo, userDid)
	if err != nil {
		return false, err
	}
	return slices.Contains(perms, perm), nil
}

func (s *Service) IsRepoCreateAllowed(ctx context.Context, host, userDid string) bool {
	return s.reader(ctx, host).isRepoCreateAllowed(ctx, host, userDid)
}

func (s *Service) KnotMembers(ctx context.Context, host string) []string {
	return s.reader(ctx, host).knotMembers(ctx, host)
}

func (s *Service) Collaborators(ctx context.Context, repo *models.Repo) []pages.Collaborator {
	return s.reader(ctx, repo.Knot).collaborators(ctx, repo)
}

func (s *Service) IsKnotMember(ctx context.Context, host, userDid string) bool {
	return s.reader(ctx, host).isKnotMember(ctx, host, userDid)
}

func (s *Service) InvalidateMembers(host string) {
	s.nat.client.InvalidateMembers(host)
}

func (s *Service) InvalidateCollaborators(host, repoDid string) {
	s.nat.client.InvalidateCollaborators(host, repoDid)
}

func (s *Service) KnotsForUser(ctx context.Context, userDid string) []string {
	legacyKnots, err := s.leg.enforcer.GetKnotsForUser(userDid)
	if err != nil {
		s.log.Error("knotsForUser: enforcer lookup failed, returning a partial list", "did", userDid, "err", err)
	}

	regs, err := db.GetRegistrations(s.nat.execer)
	if err != nil {
		s.log.Error("knotsForUser: registrations lookup failed, skipping native knots", "did", userDid, "err", err)
	}
	domains := dedup(filterMap(regs, func(r models.Registration) (string, bool) {
		return r.Domain, r.Registered != nil
	}))
	owned := filterMap(regs, func(r models.Registration) (string, bool) {
		return r.Domain, r.Registered != nil && r.ByDid == userDid
	})
	nativeMember := s.nativeMemberships(ctx, domains, userDid)

	all := make([]string, 0, len(legacyKnots)+len(owned)+len(nativeMember))
	all = append(all, legacyKnots...)
	all = append(all, owned...)
	all = append(all, nativeMember...)
	return dedup(all)
}

func (s *Service) nativeMemberships(ctx context.Context, domains []string, userDid string) []string {
	ctx, cancel := context.WithTimeout(ctx, pickerFanoutBudget)
	defer cancel()

	g, gctx := errgroup.WithContext(ctx)
	g.SetLimit(pickerFanoutConcurrency)

	var mu sync.Mutex
	var hits []string

	for _, host := range domains {
		g.Go(func() error {
			if !knotcompat.KnotHasCapability(gctx, host, s.dev, consts.CapKnotACL) {
				return nil
			}
			members, err := s.nat.client.GetKnotMembers(gctx, host)
			if err != nil || !slices.Contains(members, userDid) {
				return nil
			}
			mu.Lock()
			hits = append(hits, host)
			mu.Unlock()
			return nil
		})
	}
	_ = g.Wait()
	return hits
}

func (s *Service) repoPerms(ctx context.Context, repo *models.Repo, userDid string) []string {
	perms, _ := s.repoPermsErr(ctx, repo, userDid)
	return perms
}

func (s *Service) repoPermsErr(ctx context.Context, repo *models.Repo, userDid string) ([]string, error) {
	return s.reader(ctx, repo.Knot).repoPerms(ctx, repo, userDid)
}

func filterMap[T, U any](items []T, f func(T) (U, bool)) []U {
	var out []U
	for _, it := range items {
		if u, ok := f(it); ok {
			out = append(out, u)
		}
	}
	return out
}
