package internal

import (
	"compress/gzip"
	"errors"
	"fmt"
	"io"
	"net/http"
	"os"
	"strings"

	"gopkg.in/yaml.v3"
)

var (
	ValuesToBeInSet         = "values_to_be_in_set"
	ValuesToBeBetween       = "values_to_be_between"
	ValuesToNotBeNull       = "values_to_not_be_null"
	ValuesToMatchRegexList  = "values_to_match_regex_list"
	ValueLengthsToBeBetween = "value_lengths_to_be_between"
)

type Schema struct {
	Source string
	Events []Event `yaml:"events"`
}

type Event struct {
	Name        *string `yaml:"name"`
	Description *string `yaml:"description"`
	Fields      []Field `yaml:"fields"`
}

type Field struct {
	Name         *string            `yaml:"name"`
	Description  *string            `yaml:"description"`
	Type         *FieldType         `yaml:"type"`
	Expectations []FieldExpectation `yaml:"expectations"`
}

type FieldType struct {
	Name   string    `yaml:"name"`
	Values *[]string `yaml:"values"`
}

type FieldExpectation struct {
	Name     string   `yaml:"name"`
	Patterns []string `yaml:"patterns"`
	Min      string   `yaml:"min"`
	Max      string   `yaml:"max"`
	Values   []string `yaml:"values"`
}

// IsOptional returns true when the field is optional.
func (f *Field) IsOptional() bool {
	for _, expectation := range f.Expectations {
		if expectation.Name == ValuesToNotBeNull {
			return false
		}
	}

	return true
}

// ExpectationsCount returns the number of expectation a field has.
func (e *Event) ExpectationsCount() int {
	total := 0

	for _, field := range e.Fields {
		total += len(field.Expectations)
	}

	return total
}

// GetYamlSchemaFromURL returns a Schema from a URL that returns yaml.
func GetYamlSchemaFromURL(client *http.Client, url string) (*Schema, error) {
	var reader io.Reader

	request, err := http.NewRequest(http.MethodGet, url, nil)
	if err != nil {
		return nil, fmt.Errorf("error while building the request: %s", err)
	}

	// https://jira.xarth.tv/browse/AWSI-4601
	// Missing Accept header returns a "401 unauthorized" ...
	request.Header.Set("Accept", "text/html")
	resp, err := client.Do(request)
	if err != nil {
		return nil, fmt.Errorf("error while sending the http request: %s", err)
	}

	defer resp.Body.Close()
	if resp.StatusCode >= 300 {
		return nil, fmt.Errorf("error while fetching the schema from %s: %s", url, resp.Status)
	}

	// redirected to the midway authentication page.
	if strings.Contains(resp.Header.Get("Content-Type"), "text/html") {
		return nil, errors.New("expired midway credentials please run: mwinit --aea")
	}

	reader = resp.Body
	if strings.HasSuffix(url, ".gz") || resp.Header.Get("Content-Encoding") == "gzip" {
		gzr, err := gzip.NewReader(resp.Body)
		if err != nil {
			return nil, err
		}

		defer gzr.Close()
		reader = gzr
	}

	return GetYamlSchema(url, reader)
}

// GetYamlSchemaFromDisk returns a Schema from a saved schema file
func GetYamlSchemaFromDisk(location string) (*Schema, error) {
	file, err := os.Open(location)
	if err != nil {
		return nil, err
	}

	return GetYamlSchema(location, file)
}

// GetYamlSchema returns a Schema from any io.reader
func GetYamlSchema(source string, reader io.Reader) (*Schema, error) {
	body, err := io.ReadAll(reader)
	if err != nil {
		return nil, err
	}

	s := Schema{Source: source}
	err = yaml.Unmarshal(body, &s)
	if err != nil {
		return nil, err
	}

	return &s, nil
}

// HasFieldWithExpectation returns true if the schema has field with the given expectation
func (s *Schema) HasFieldWithExpectation(expectationName string) bool {
	for _, event := range s.Events {
		for _, field := range event.Fields {
			for _, expectation := range field.Expectations {
				if expectation.Name == expectationName {
					return true
				}
			}
		}
	}
	return false
}

// HasAnyExpectations returns true if the schema has at least one field with at least one expectation
func (s *Schema) HasAnyExpectations() bool {
	for _, event := range s.Events {
		if event.ExpectationsCount() > 0 {
			return true
		}
	}
	return false
}
