package schemas

import (
	"context"
	"errors"
	"fmt"
	"net/http"
	"net/url"
	"os"
	"path"
	"path/filepath"
	"strings"
)

var (
	ErrCannotResolveSchema      = errors.New("cannot resolve schema")
	ErrCannotLoadSchema         = errors.New("cannot load schema")
	ErrUnsupportedContentType   = errors.New("unsupported content type")
	ErrUnsupportedFileExtension = errors.New("unsupported file extension")
	ErrUnsupportedURL           = errors.New("unsupported URL")
)

type Loader interface {
	Load(uri, parentURI string) (*Schema, error)
}

func NewCachedLoader(loader Loader, cache map[string]*Schema) *CachedLoader {
	return &CachedLoader{
		loader: loader,
		cache:  cache,
	}
}

type CachedLoader struct {
	loader Loader
	cache  map[string]*Schema
}

func (l *CachedLoader) Load(uri, parentURI string) (*Schema, error) {
	if schema, ok := l.cache[uri]; ok {
		return schema, nil
	}

	schema, err := l.loader.Load(uri, parentURI)
	if err != nil {
		return nil, errors.Join(ErrCannotLoadSchema, err)
	}

	l.cache[uri] = schema

	return schema, nil
}

func NewFileLoader(resolveExtensions, yamlExtensions []string) *FileLoader {
	return &FileLoader{
		resolveExtensions: resolveExtensions,
		yamlExtensions:    toExtensionSet(yamlExtensions),
	}
}

type FileLoader struct {
	resolveExtensions []string
	yamlExtensions    map[string]bool
}

func (l *FileLoader) Load(fileName, parentFileName string) (*Schema, error) {
	qualified, err := QualifiedFileName(fileName, parentFileName, l.resolveExtensions)
	if err != nil {
		return nil, err
	}

	schema, err := l.parseFile(qualified)
	if err != nil {
		return nil, err
	}

	return schema, nil
}

func (l *FileLoader) parseFile(fileName string) (*Schema, error) {
	if l.yamlExtensions[path.Ext(fileName)] {
		sc, err := FromYAMLFile(fileName)
		if err != nil {
			return nil, fmt.Errorf("error parsing YAML file %s: %w", fileName, err)
		}

		return sc, nil
	}

	sc, err := FromJSONFile(fileName)
	if err != nil {
		return nil, fmt.Errorf("error parsing JSON file %s: %w", fileName, err)
	}

	return sc, nil
}

func NewDefaultCacheLoader(resolveExtensions, yamlExtensions []string) *CachedLoader {
	return NewCachedLoader(NewDefaultMultiLoader(resolveExtensions, yamlExtensions), map[string]*Schema{})
}

func NewDefaultMultiLoader(resolveExtensions, yamlExtensions []string) MultiLoader {
	httpLoader := NewHTTPLoader(yamlExtensions)

	return MultiLoader{
		RefTypeFile:  NewFileLoader(resolveExtensions, yamlExtensions),
		RefTypeHTTP:  httpLoader,
		RefTypeHTTPS: httpLoader,
	}
}

type MultiLoader map[RefType]Loader

func (l MultiLoader) Load(uri, parentURI string) (*Schema, error) {
	ref, err := GetRefType(uri)
	if err != nil {
		return nil, err
	}

	loader, ok := l[ref]
	if !ok {
		return nil, ErrUnsupportedRefFormat
	}

	schema, err := loader.Load(uri, parentURI)
	if err != nil {
		return nil, fmt.Errorf("failed to load schema %q: %w", uri, err)
	}

	return schema, nil
}

func NewHTTPLoader(yamlExtensions []string) *HTTPLoader {
	return &HTTPLoader{YAMLExtensions: toExtensionSet(yamlExtensions)}
}

type HTTPLoader struct {
	YAMLExtensions map[string]bool
}

func (l *HTTPLoader) Load(uri, parentURI string) (*Schema, error) {
	u, err := url.Parse(uri)
	if err != nil {
		return nil, fmt.Errorf("failed to parse url: %w", err)
	}

	if u.Scheme == "http" || u.Scheme == "https" {
		req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, u.String(), nil)
		if err != nil {
			return nil, fmt.Errorf("failed to create request: %w", err)
		}

		resp, err := (&http.Client{}).Do(req)
		if err != nil {
			return nil, fmt.Errorf("failed to perform request: %w", err)
		}

		defer func() {
			if resp != nil && resp.Body != nil {
				_ = resp.Body.Close()
			}
		}()

		switch resp.Header.Get("Content-Type") {
		case "application/json":
			return FromJSONReader(resp.Body)

		case "application/yaml", "application/x-yaml", "text/yaml", "text/x-yaml":
			return FromYAMLReader(resp.Body)

		default:
			if l.YAMLExtensions[path.Ext(u.Path)] {
				return FromYAMLReader(resp.Body)
			}

			return FromJSONReader(resp.Body)
		}
	}

	return nil, fmt.Errorf("%w: %q", ErrUnsupportedURL, uri)
}

func QualifiedFileName(fileName, parentFileName string, resolveExtensions []string) (string, error) {
	r, err := GetRefType(fileName)
	if err != nil {
		return "", err
	}

	if r != RefTypeFile {
		return fileName[strings.Index(fileName, "://")+3:], nil
	}

	fileName = strings.TrimPrefix(fileName, "file://")

	if !filepath.IsAbs(fileName) {
		fileName = filepath.Join(filepath.Dir(parentFileName), fileName)
	}

	exts := append([]string{""}, resolveExtensions...)
	for _, ext := range exts {
		qualified := fileName + ext

		if !fileExists(qualified) {
			continue
		}

		var err error

		qualified, err = filepath.EvalSymlinks(qualified)
		if err != nil {
			return "", fmt.Errorf("error resolving symlinks in %s: %w", qualified, err)
		}

		return qualified, nil
	}

	return "", fmt.Errorf("%w %q", ErrCannotResolveSchema, fileName)
}

func fileExists(fileName string) bool {
	_, err := os.Stat(fileName)

	return err == nil || !os.IsNotExist(err)
}

func toExtensionSet(items []string) map[string]bool {
	set := make(map[string]bool, len(items))

	for _, item := range items {
		if !strings.HasPrefix(item, ".") {
			item = "." + item
		}

		set[item] = true
	}

	return set
}
