package jsonschema

import (
	"fmt"
	"slices"
	"strings"
)

type root struct {
	url                 url
	doc                 any
	resources           map[jsonPointer]*resource
	subschemasProcessed map[jsonPointer]struct{}
}

func (r *root) rootResource() *resource {
	return r.resources[""]
}

func (r *root) resource(ptr jsonPointer) *resource {
	for {
		if res, ok := r.resources[ptr]; ok {
			return res
		}
		slash := strings.LastIndexByte(string(ptr), '/')
		if slash == -1 {
			break
		}
		ptr = ptr[:slash]
	}
	return r.rootResource()
}

func (r *root) resolveFragmentIn(frag fragment, res *resource) (urlPtr, error) {
	var ptr jsonPointer
	switch f := frag.convert().(type) {
	case jsonPointer:
		ptr = res.ptr.concat(f)
	case anchor:
		aptr, ok := res.anchors[f]
		if !ok {
			return urlPtr{}, &AnchorNotFoundError{
				URL:       r.url.String(),
				Reference: (&urlFrag{res.id, frag}).String(),
			}
		}
		ptr = aptr
	}
	return urlPtr{r.url, ptr}, nil
}

func (r *root) resolveFragment(frag fragment) (urlPtr, error) {
	return r.resolveFragmentIn(frag, r.rootResource())
}

// resovles urlFrag to urlPtr from root.
// returns nil if it is external.
func (r *root) resolve(uf urlFrag) (*urlPtr, error) {
	var res *resource
	if uf.url == r.url {
		res = r.rootResource()
	} else {
		// look for resource with id==uf.url
		for _, v := range r.resources {
			if v.id == uf.url {
				res = v
				break
			}
		}
		if res == nil {
			return nil, nil // external url
		}
	}
	up, err := r.resolveFragmentIn(uf.frag, res)
	return &up, err
}

func (r *root) collectAnchors(sch any, schPtr jsonPointer, res *resource) error {
	obj, ok := sch.(map[string]any)
	if !ok {
		return nil
	}

	addAnchor := func(anchor anchor) error {
		ptr1, ok := res.anchors[anchor]
		if ok {
			if ptr1 == schPtr {
				// anchor with same root_ptr already exists
				return nil
			}
			return &DuplicateAnchorError{
				string(anchor), r.url.String(), string(ptr1), string(schPtr),
			}
		}
		res.anchors[anchor] = schPtr
		return nil
	}

	if res.dialect.draft.version < 2019 {
		if _, ok := obj["$ref"]; ok {
			// All other properties in a "$ref" object MUST be ignored
			return nil
		}
		// anchor is specified in id
		if id, ok := strVal(obj, res.dialect.draft.id); ok {
			_, frag, err := splitFragment(id)
			if err != nil {
				loc := urlPtr{r.url, schPtr}
				return &ParseAnchorError{loc.String()}
			}
			if anchor, ok := frag.convert().(anchor); ok {
				if err := addAnchor(anchor); err != nil {
					return err
				}
			}
		}
	}
	if res.dialect.draft.version >= 2019 {
		if s, ok := strVal(obj, "$anchor"); ok {
			if err := addAnchor(anchor(s)); err != nil {
				return err
			}
		}
	}
	if res.dialect.draft.version >= 2020 {
		if s, ok := strVal(obj, "$dynamicAnchor"); ok {
			if err := addAnchor(anchor(s)); err != nil {
				return err
			}
			res.dynamicAnchors = append(res.dynamicAnchors, anchor(s))
		}
	}

	return nil
}

func (r *root) clone() *root {
	processed := map[jsonPointer]struct{}{}
	for k := range r.subschemasProcessed {
		processed[k] = struct{}{}
	}
	resources := map[jsonPointer]*resource{}
	for k, v := range r.resources {
		resources[k] = v.clone()
	}
	return &root{
		url:                 r.url,
		doc:                 r.doc,
		resources:           resources,
		subschemasProcessed: processed,
	}
}

// --

type resource struct {
	ptr            jsonPointer
	id             url
	dialect        dialect
	anchors        map[anchor]jsonPointer
	dynamicAnchors []anchor
}

func newResource(ptr jsonPointer, id url) *resource {
	return &resource{ptr: ptr, id: id, anchors: make(map[anchor]jsonPointer)}
}

func (res *resource) clone() *resource {
	anchors := map[anchor]jsonPointer{}
	for k, v := range res.anchors {
		anchors[k] = v
	}
	return &resource{
		ptr:            res.ptr,
		id:             res.id,
		dialect:        res.dialect,
		anchors:        anchors,
		dynamicAnchors: slices.Clone(res.dynamicAnchors),
	}
}

//--

type UnsupportedVocabularyError struct {
	URL        string
	Vocabulary string
}

func (e *UnsupportedVocabularyError) Error() string {
	return fmt.Sprintf("unsupported vocabulary %q in %q", e.Vocabulary, e.URL)
}

// --

type AnchorNotFoundError struct {
	URL       string
	Reference string
}

func (e *AnchorNotFoundError) Error() string {
	return fmt.Sprintf("anchor in %q not found in schema %q", e.Reference, e.URL)
}
