package jsonschema

import (
	"errors"
	"fmt"
	"io/ioutil"
	"net/http"
	"net/url"
	"strings"

	"github.com/buger/jsonparser"
)

// Relevant docs:
// https://json-schema.org/understanding-json-schema/structuring.html

// Anything starting with # means:
// Go to base schema -> find whatever is after #

func unescapeRefPath(refPath string) string {
	var err error
	var ref, frag string

	refParts := strings.Split(refPath, "#")
	if len(refParts) == 1 {
		frag = refParts[0]
	} else if len(refParts) > 1 {
		ref = refParts[0]
		frag = strings.Join(refParts[1:], "#")
	}

	frag, err = url.QueryUnescape(frag)
	if err != nil {
		return ""
	}
	frag = strings.ReplaceAll(frag, "~0", "~")
	frag = strings.ReplaceAll(frag, "~1", "/")

	if ref != "" && frag != "" {
		return ref + "#" + frag
	}

	return ref + frag
}

func escapeRefPath(refPath string) string {
	var err error
	var ref, frag string

	refParts := strings.Split(refPath, "#")
	if len(refParts) == 1 {
		frag = refParts[0]
	} else if len(refParts) > 1 {
		ref = refParts[0]
		frag = strings.Join(refParts[1:], "#")
	}

	frag, err = url.QueryUnescape(frag)
	if err != nil {
		return ""
	}
	frag = strings.ReplaceAll(frag, "~", "~0")
	frag = strings.ReplaceAll(frag, "/", "~1")

	if ref != "" && frag != "" {
		return ref + "#" + frag
	}

	return ref + frag
}

// ExpandURI attempts to resolve a uri against the current Base URI
func (s *Schema) ExpandURI(uri string) (*url.URL, error) {
	// If uri is empty, it is seen as invalid
	if len(uri) == 0 {
		return nil, errors.New("URI is invalid")
	}

	// If uri starts with #, it should not be expanded further
	if len(uri) > 0 && uri[:1] == "#" {
		return url.Parse(uri)
	}

	// Parse uri
	uriParsed, err := url.Parse(uri)
	if err != nil {
		return nil, errors.New("URI is invalid")
	}

	// If uri starts with http or https it is already expanded as much as possible
	if uriParsed.Scheme == "http" || uriParsed.Scheme == "https" {
		return uriParsed, nil
	}

	// Try to get the current Base URI
	var curBaseURI *url.URL
	if s != nil {
		if s.baseURI != nil {
			curBaseURI = s.baseURI
		} else if s.base != nil && s.base.baseURI != nil {
			curBaseURI = s.base.baseURI
		}
	}

	// If both Base URI and uri is valid, we'll merge and return the result
	if curBaseURI != nil {
		path := uriParsed.Path
		if uriParsed.Fragment != "" {
			path += "#" + uriParsed.Fragment
		}
		return curBaseURI.Parse(path)
	}

	// If Base URI is not found, uri is returned as is
	return uriParsed, nil
}

func (s *Schema) AddSchema(schema *Schema) error {
	return s.AddSchemaString(schema.String())
}

func (s *Schema) AddSchemaString(schemaString string) error {
	if s == nil {
		return nil
	}

	_, err := s.Parse([]byte(schemaString))
	return err
}

func (s *Schema) DeRef() error {
	var err error

	if s == nil {
		return nil
	}

	root := s
	if s.root != nil {
		root = s.root
	}

	if root.refs != nil {
		for _, ref := range *root.refs {
			if ref.Schema == nil {
				ref.Schema, err = ref.parent.ResolveRef(ref)
				if err != nil {
					return err
				}
			}
		}
	}

	return nil
}

func (s *Schema) ResolveRef(ref *Ref) (*Schema, error) {
	// If Schema is set, it's a cached version
	if ref.Schema != nil {
		return ref.Schema, nil
	}

	// If String is nil or empty, the $ref is invalid
	if ref.String == nil || len(*ref.String) == 0 {
		return nil, errors.New("$ref is invalid")
	}

	refStr := *ref.String

	baseSchema := s
	if s != nil && s.baseURI == nil && s.base != nil {
		baseSchema = s.base
	}

	// Check for base schema reference
	if refStr[:1] == "#" {
		// Check if this is a base schema reference with no path to a sub schema
		if refStr == "#" {
			return baseSchema, nil
		}

		if len(refStr) > 1 && refStr[:2] != "#/" {
			refSchema := baseSchema.getPointer(refStr)
			if refSchema != nil {
				return refSchema, nil
			}

			return nil, fmt.Errorf("unable to find ref: %s", refStr)
		}

		// Find the path
		pathParts := strings.Split(strings.Trim(refStr[1:], "/"), "/")
		if len(pathParts) == 0 {
			return baseSchema, nil
		}

		for i := 0; i < len(pathParts); i++ {
			pathParts[i] = unescapeRefPath(pathParts[i])

			switch pathParts[i] {
			case "definitions":
				if i >= (len(pathParts) - 1) {
					return nil, errors.New("#/definitions is not a valid schema")
				}
				i++
				pathParts[i] = unescapeRefPath(pathParts[i])
				baseProp, ok := (*baseSchema.Definitions).GetProperty(pathParts[i])
				if ok {
					baseSchema = baseProp.Property
				}

			case "properties":
				if i >= (len(pathParts) - 1) {
					return nil, errors.New("#/properties is not a valid schema")
				}
				i++
				pathParts[i] = unescapeRefPath(pathParts[i])
				baseProp, ok := (*baseSchema.Properties).GetProperty(pathParts[i])
				if ok {
					baseSchema = baseProp.Property
				}

			case "items":
				if baseSchema.Items.Boolean != nil {
					baseSchema = &Schema{boolean: baseSchema.boolean}
				} else if baseSchema.Items.Schema != nil {
					baseSchema = baseSchema.Items.Schema
				}

				if i >= (len(pathParts) - 1) {
					return nil, errors.New("#/items is not a valid schema")
				}
				i++

				idx, err := jsonparser.ParseInt([]byte(pathParts[i]))
				if err != nil {
					return nil, errors.New("unable to parse item's index")
				}
				baseSchema = (*baseSchema.Items.Schemas)[idx]

			default:
				if isInteger([]byte(pathParts[i])) {
					pathParts[i] = fmt.Sprintf("[%s]", pathParts[i])
				} else {
					pathParts[i] = unescapeRefPath(pathParts[i])
				}

				rawBase, _, _, err := jsonparser.Get(baseSchema.raw, pathParts[i])
				if err != nil {
					return nil, fmt.Errorf("unable to find schema at path: %s", *ref.String)
				}

				baseSchema, err = baseSchema.Parse(rawBase)
				if err != nil {
					return nil, fmt.Errorf("unable to find schema at path: %s", *ref.String)
				}

			}
		}

		return baseSchema, nil

	} else {
		refURI, err := baseSchema.ExpandURI(refStr)
		if err != nil {
			return nil, err
		}

		switch refURI.String() {
		case "http://json-schema.org/draft-04/schema":
			baseSchema = Draft04Schema
		case "http://json-schema.org/draft-05/schema":
			baseSchema = Draft04Schema
		case "http://json-schema.org/schema":
			baseSchema = Draft04Schema
		case "http://json-schema.org/draft-06/schema":
			baseSchema = Draft06Schema
		case "http://json-schema.org/draft-07/schema":
			baseSchema = Draft07Schema
		default:
			if s != nil {
				frag := refURI.Fragment
				refURI.Fragment = ""
				baseSchema = s.getPointer(refURI.String())
				if baseSchema != nil && frag != "" {
					frag = "#" + frag
					return baseSchema.ResolveRef(&Ref{String: &frag})
				}
				refURI.Fragment = frag
			}
		}

		// Fetch the schema
		if baseSchema == nil {
			if err != nil {
				return nil, addError(errors.New("ref contains invalid URL"), err)
			}

			// TODO: We should probably accept query params
			schemaURL := fmt.Sprintf(
				"%s://%s%s%s",
				refURI.Scheme,
				refURI.User.String(),
				refURI.Host,
				refURI.Path,
			)

			res, err := http.Get(schemaURL)
			if err != nil {
				return nil, err
			}
			body, err := ioutil.ReadAll(res.Body)
			if err != nil {
				return nil, err
			}
			res.Body.Close()

			// Add the $id to the schema, if it does not exist
			idKey := "$id"
			if s != nil && s.root != nil {
				if s.root.IsDraft4() {
					idKey = "id"
				}
			}

			_, _, _, err = jsonparser.Get(body, idKey)
			if err != nil {
				id := []byte(`"` + strings.ReplaceAll(schemaURL, `"`, `\"`) + `"`)
				body, err = jsonparser.Set(body, id, idKey)
				if err != nil {
					return nil, err
				}
			}

			baseSchema, err := baseSchema.Parse(body)
			if err != nil {
				return nil, err
			}
			refURI.Fragment = ""
			baseSchema.baseURI = refURI
			// log.Println(baseSchema)
			s.setPointer(refURI.String(), baseSchema)

			return baseSchema, nil
		}

		if baseSchema == nil {
			return nil, errors.New("unable to fetch the specified schema: " + refStr)
		}

		if refURI.Fragment != "" {
			fragment := fmt.Sprintf("#%s", refURI.Fragment)
			return baseSchema.ResolveRef(&Ref{String: &fragment})
		}

		return baseSchema, err
	}
}
