package storage

import (
	"encoding/json"
	"fmt"
	"os"
	"strings"
)

const AdditionalAuthoritiesEnv = "FILE_STORAGE_ADDITIONAL_S3_AUTHORITIES_JSON"

type Authority struct {
	Name       string
	SourceType string
	Config     Config
	Primary    bool
}

type AuthorityCatalog struct {
	primary    Authority
	additional map[string]Authority
	governed   map[string]struct{}
}

type additionalAuthorityDocument struct {
	Name       string `json:"name"`
	SourceType string `json:"source_type"`
	Provider   string `json:"provider"`
	Bucket     string `json:"bucket"`
	Region     string `json:"region"`
	KeyPrefix  string `json:"key_prefix"`
}

func LoadAuthorityCatalogFromEnv(primary Config) (*AuthorityCatalog, error) {
	return NewAuthorityCatalog(primary, os.Getenv(AdditionalAuthoritiesEnv))
}

func NewAuthorityCatalog(primary Config, raw string) (*AuthorityCatalog, error) {
	primary.Provider = strings.ToLower(strings.TrimSpace(primary.Provider))
	primary.Bucket = strings.TrimSpace(primary.Bucket)
	primary.Region = strings.TrimSpace(primary.Region)
	primary.KeyPrefix = normalizeAuthorityKeyPrefix(primary.KeyPrefix)
	catalog := &AuthorityCatalog{
		primary: Authority{
			Name:    "runtime",
			Config:  primary,
			Primary: true,
		},
		additional: map[string]Authority{},
		governed:   map[string]struct{}{},
	}
	if primary.Provider != "" && primary.Bucket != "" {
		catalog.governed[authorityLocationKey(primary.Provider, primary.Bucket)] = struct{}{}
	}

	raw = strings.TrimSpace(raw)
	if len(raw) >= 2 && raw[0] == '\'' && raw[len(raw)-1] == '\'' {
		raw = raw[1 : len(raw)-1]
	}
	if raw == "" {
		return catalog, nil
	}
	var documents []additionalAuthorityDocument
	if err := json.Unmarshal([]byte(raw), &documents); err != nil {
		return nil, fmt.Errorf("%s must be valid JSON: %w", AdditionalAuthoritiesEnv, err)
	}
	seenNames := map[string]struct{}{"runtime": {}}
	for index, document := range documents {
		document.Name = strings.TrimSpace(document.Name)
		document.SourceType = strings.ToLower(strings.TrimSpace(document.SourceType))
		document.Provider = strings.ToLower(strings.TrimSpace(document.Provider))
		if document.Provider == "" {
			document.Provider = ProviderS3
		}
		document.Bucket = strings.TrimSpace(document.Bucket)
		document.Region = strings.TrimSpace(document.Region)
		document.KeyPrefix = normalizeAuthorityKeyPrefix(document.KeyPrefix)
		missing := make([]string, 0, 5)
		for _, field := range []struct{ name, value string }{
			{name: "name", value: document.Name},
			{name: "source_type", value: document.SourceType},
			{name: "bucket", value: document.Bucket},
			{name: "region", value: document.Region},
			{name: "key_prefix", value: document.KeyPrefix},
		} {
			if field.value == "" {
				missing = append(missing, field.name)
			}
		}
		if len(missing) > 0 {
			return nil, fmt.Errorf("%s[%d] is missing required authority fields", AdditionalAuthoritiesEnv, index)
		}
		if document.Provider != ProviderS3 {
			return nil, fmt.Errorf("%s[%d] provider must be %s", AdditionalAuthoritiesEnv, index, ProviderS3)
		}
		if !validAuthorityKeyPrefix(document.KeyPrefix) {
			return nil, fmt.Errorf("%s[%d] has an invalid key_prefix", AdditionalAuthoritiesEnv, index)
		}
		if _, exists := seenNames[document.Name]; exists {
			return nil, fmt.Errorf("%s has duplicate authority name %q", AdditionalAuthoritiesEnv, document.Name)
		}
		key := authorityBindingKey(document.SourceType, document.Provider, document.Bucket)
		if _, exists := catalog.additional[key]; exists {
			return nil, fmt.Errorf("%s has an ambiguous source_type, provider, and bucket binding", AdditionalAuthoritiesEnv)
		}
		config := primary
		config.Provider = document.Provider
		config.Bucket = document.Bucket
		config.Region = document.Region
		config.KeyPrefix = document.KeyPrefix
		catalog.additional[key] = Authority{
			Name:       document.Name,
			SourceType: document.SourceType,
			Config:     config,
		}
		catalog.governed[authorityLocationKey(document.Provider, document.Bucket)] = struct{}{}
		seenNames[document.Name] = struct{}{}
	}
	return catalog, nil
}

func (c *AuthorityCatalog) Resolve(provider, bucket, sourceType string) (Authority, bool) {
	if c == nil {
		return Authority{}, false
	}
	provider = strings.ToLower(strings.TrimSpace(provider))
	bucket = strings.TrimSpace(bucket)
	sourceType = strings.ToLower(strings.TrimSpace(sourceType))
	if authority, ok := c.additional[authorityBindingKey(sourceType, provider, bucket)]; ok {
		return authority, true
	}
	if provider == c.primary.Config.Provider && bucket == c.primary.Config.Bucket {
		return c.primary, true
	}
	return Authority{}, false
}

func (c *AuthorityCatalog) Governs(provider, bucket string) bool {
	if c == nil {
		return false
	}
	_, ok := c.governed[authorityLocationKey(provider, bucket)]
	return ok
}

func authorityBindingKey(sourceType, provider, bucket string) string {
	return strings.ToLower(strings.TrimSpace(sourceType)) + "\x00" +
		strings.ToLower(strings.TrimSpace(provider)) + "\x00" + strings.TrimSpace(bucket)
}

func authorityLocationKey(provider, bucket string) string {
	return strings.ToLower(strings.TrimSpace(provider)) + "\x00" + strings.TrimSpace(bucket)
}

func normalizeAuthorityKeyPrefix(value string) string {
	return strings.Trim(strings.TrimSpace(strings.ReplaceAll(value, "\\", "/")), "/")
}

func validAuthorityKeyPrefix(value string) bool {
	if value == "" {
		return false
	}
	for _, part := range strings.Split(value, "/") {
		if part == "" || part == "." || part == ".." {
			return false
		}
	}
	return true
}
