package loader

import (
	"context"
	"maps"
	"slices"
	"strings"
	"sync"

	ld "sourcery.dny.nu/longdistance"
	"sourcery.dny.nu/pana/internal/json"
	"sourcery.dny.nu/pana/vocab/geojson"
	gts "sourcery.dny.nu/pana/vocab/gotosocial"
	"sourcery.dny.nu/pana/vocab/litepub"
	as "sourcery.dny.nu/pana/vocab/w3/activitystreams"
	credv1 "sourcery.dny.nu/pana/vocab/w3id/credentialsv1"
	idv1 "sourcery.dny.nu/pana/vocab/w3id/identityv1"
	secv1 "sourcery.dny.nu/pana/vocab/w3id/securityv1"
)

// Builtin is a remote context cache.
type Builtin struct {
	domains  map[string]ld.Document
	paths    map[string]ld.Document
	pathKeys []string

	lock sync.RWMutex
}

func New() *Builtin {
	b := &Builtin{
		domains: make(map[string]ld.Document, 8),
		paths:   make(map[string]ld.Document, 2),
	}
	b.RegisterContextURL(as.IRI, as.ContextDocument)
	b.RegisterContextURL(gts.IRI, gts.ContextDocument)
	b.RegisterContextURL(credv1.IRI, credv1.ContextDocument)
	b.RegisterContextURL(idv1.IRI, idv1.ContextDocument)
	b.RegisterContextURL(secv1.IRI, secv1.ContextDocument)
	b.RegisterContextURL(litepub.IRI, litepub.ContextDocument)
	b.RegisterContextURL(geojson.IRI, geojson.ContextDocument)
	b.RegisterContextPath("/litepub-0.1.jsonld", litepub.ContextDocument1dot0)
	return b
}

// Get is an [ld.RemoteContextLoaderFunc].
func (b *Builtin) Get(_ context.Context, url string) (ld.Document, error) {
	b.lock.RLock()
	defer b.lock.RUnlock()

	if doc, ok := b.domains[url]; ok {
		return doc, nil
	}

	for _, path := range b.pathKeys {
		if strings.HasSuffix(url, path) {
			doc := b.paths[path]
			doc.URL = url
			return doc, nil
		}
	}

	return ld.Document{}, ld.ErrLoadingRemoteContext
}

// RegisterContextURL adds or overrides a context document for the specified
// remote context URL in the loader.
func (b *Builtin) RegisterContextURL(url string, doc json.RawMessage) error {
	b.lock.Lock()
	defer b.lock.Unlock()

	ctx, err := json.GetContextDocument(doc)
	if err != nil {
		return err
	}

	b.domains[url] = ld.Document{URL: url, Context: ctx}
	return nil
}

// RegisterContextPath adds or overrides a context document for the specified
// remote path in the loader.
//
// Paths are always matches as URL suffixes.
func (b *Builtin) RegisterContextPath(path string, doc json.RawMessage) error {
	b.lock.Lock()
	defer b.lock.Unlock()

	ctx, err := json.GetContextDocument(doc)
	if err != nil {
		return err
	}

	b.paths[path] = ld.Document{URL: "", Context: ctx}
	b.pathKeys = slices.Collect(maps.Keys(b.paths))
	return nil
}
