// Copyright 2018 johandorland ( https://github.com/johandorland )
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//   http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package gojsonschema

import (
	"bytes"
	"context"
	"errors"

	"github.com/xeipuuv/gojsonreference"
)

// SchemaLoader is used to load schemas
type SchemaLoader struct {
	pool             *schemaPool
	AutoDetect       bool
	Validate         bool
	Draft            Draft
	ValidatePatterns bool
	// AllowNet is the list of hosts that remote references may be fetched
	// from. A nil list permits any host; a non-nil empty list permits none.
	AllowNet []string
	// Context, when set, aborts in-flight remote reference fetches. Callers
	// that have one -- evaluation, which holds the query's context -- should
	// pass it so a cancelled or timed-out query doesn't leave requests
	// running behind it.
	Context context.Context
}

// NewSchemaLoader creates a new NewSchemaLoader
func NewSchemaLoader() *SchemaLoader {
	ps := &SchemaLoader{
		pool:       &schemaPool{schemaPoolDocuments: make(map[string]*schemaPoolDocument)},
		AutoDetect: true,
		Validate:   false,
		Draft:      Hybrid,
	}
	ps.pool.autoDetect = &ps.AutoDetect

	return ps
}

func (sl *SchemaLoader) validateMetaschema(documentNode any) (err error) {
	var schema string
	if sl.AutoDetect {
		if schema, _, err = parseSchemaURL(documentNode); err != nil {
			return err
		}
	}

	// If no explicit "$schema" is used, use the default metaschema associated with the draft used
	if schema == "" {
		if sl.Draft == Hybrid {
			return nil
		}
		schema = drafts.GetSchemaURL(sl.Draft)
	}

	//Disable validation when loading the metaschema to prevent an infinite recursive loop
	sl.Validate = false

	metaSchema, err := sl.Compile(NewReferenceLoader(schema))
	if err != nil {
		return err
	}

	sl.Validate = true

	result := metaSchema.validateDocument(documentNode)

	if !result.Valid() {
		var res bytes.Buffer
		for _, err := range result.Errors() {
			res.WriteString(err.String())
			res.WriteByte('\n')
		}
		return errors.New(res.String())
	}

	return nil
}

// AddSchemas adds an arbritrary amount of schemas to the schema cache. As this function does not require
// an explicit URL, every schema should contain an $id, so that it can be referenced by the main schema
func (sl *SchemaLoader) AddSchemas(loaders ...JSONLoader) error {
	emptyRef, _ := gojsonreference.NewJsonReference("")

	for _, loader := range loaders {
		doc, err := loader.LoadJSON()
		if err != nil {
			return err
		}

		if sl.Validate {
			if err := sl.validateMetaschema(doc); err != nil {
				return err
			}
		}

		// Directly use the Recursive function, so that it get only added to the schema Pool by $id
		// and not by the ref of the document as it's empty
		if err = sl.pool.parseReferences(doc, emptyRef, false); err != nil {
			return err
		}
	}

	return nil
}

// AddSchema adds a schema under the provided URL to the schema cache
func (sl *SchemaLoader) AddSchema(url string, loader JSONLoader) error {
	ref, err := gojsonreference.NewJsonReference(url)
	if err != nil {
		return err
	}

	doc, err := loader.LoadJSON()
	if err != nil {
		return err
	}

	if sl.Validate {
		if err := sl.validateMetaschema(doc); err != nil {
			return err
		}
	}

	return sl.pool.parseReferences(doc, ref, true)
}

// Compile loads and compiles a schema
func (sl *SchemaLoader) Compile(rootSchema JSONLoader) (*Schema, error) {
	ref, err := rootSchema.JSONReference()
	if err != nil {
		return nil, err
	}

	d := Schema{}
	d.Pool = sl.pool
	// NewStringLoader and NewGoLoader hand back unrestricted factories, so the
	// limits come from the compilation, not the root loader. The pool resolves
	// every $ref through this factory, however deeply nested.
	d.Pool.jsonLoaderFactory = rootSchema.LoaderFactory().withRemoteRefLimits(remoteRefLimits{
		allowNet: newAllowNetSet(sl.AllowNet),
		ctx:      sl.Context,
	})
	d.DocumentReference = ref
	d.ReferencePool = newSchemaReferencePool()
	d.validatePatterns = sl.ValidatePatterns

	var doc any
	if ref.String() != "" {
		// Get document from schema pool
		spd, err := d.Pool.GetDocument(d.DocumentReference)
		if err != nil {
			return nil, err
		}
		doc = spd.Document
	} else {
		// Load JSON directly
		if doc, err = rootSchema.LoadJSON(); err != nil {
			return nil, err
		}
		// References need only be parsed if loading JSON directly
		// as pool.GetDocument already does this for us if loading by reference
		if err = sl.pool.parseReferences(doc, ref, true); err != nil {
			return nil, err
		}
	}

	if sl.Validate {
		if err := sl.validateMetaschema(doc); err != nil {
			return nil, err
		}
	}

	draft := sl.Draft
	if sl.AutoDetect {
		_, detectedDraft, err := parseSchemaURL(doc)
		if err != nil {
			return nil, err
		}
		if detectedDraft != nil {
			draft = *detectedDraft
		}
	}

	if err = d.parse(doc, draft); err != nil {
		return nil, err
	}

	return &d, nil
}
