package kb

import (
	"bytes"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"unicode/utf8"
)

var (
	ErrUADashboardArchiveUnsupported = errors.New("dashboard-archive-unsupported")
	ErrUADashboardArchiveUnsafe      = errors.New("dashboard-archive-unsafe")
	ErrUADashboardArchiveLayout      = errors.New("dashboard-archive-layout-invalid")
	ErrUADashboardArchiveLimit       = errors.New("dashboard-archive-limit-exceeded")
	ErrUADashboardDataInvalid        = errors.New("dashboard-data-invalid")
)

const (
	UADashboardKnowledgeGraphFile = "knowledge-graph.json"
	UADashboardDomainGraphFile    = "domain-graph.json"
	UADashboardMetaFile           = "meta.json"
	UADashboardConfigFile         = "config.json"
	UADashboardFingerprintsFile   = "fingerprints.json"
	UADashboardDiffOverlayFile    = "diff-overlay.json"
)

var uaDashboardAllowedFiles = map[string]struct{}{
	UADashboardKnowledgeGraphFile: {},
	UADashboardDomainGraphFile:    {},
	UADashboardMetaFile:           {},
	UADashboardConfigFile:         {},
	UADashboardFingerprintsFile:   {},
}

var uaDashboardHistoricalFiles = map[string]struct{}{
	UADashboardKnowledgeGraphFile: {},
	UADashboardDomainGraphFile:    {},
	UADashboardMetaFile:           {},
	UADashboardConfigFile:         {},
	UADashboardFingerprintsFile:   {},
	UADashboardDiffOverlayFile:    {},
}

func hasExactUADashboardFiles(files map[string][]byte) bool {
	paths := make([]string, 0, len(files))
	for path := range files {
		paths = append(paths, path)
	}
	return hasExactUADashboardPaths(paths)
}

func hasExactUADashboardPaths(paths []string) bool {
	if len(paths) != len(uaDashboardAllowedFiles) {
		return false
	}
	return hasUniquePathsFromSet(paths, uaDashboardAllowedFiles)
}

func hasKnownHistoricalUADashboardPaths(paths []string) bool {
	if len(paths) == 0 {
		return false
	}
	return hasUniquePathsFromSet(paths, uaDashboardHistoricalFiles)
}

func hasUniquePathsFromSet(paths []string, allowed map[string]struct{}) bool {
	seen := make(map[string]struct{}, len(paths))
	for _, path := range paths {
		if _, ok := allowed[path]; !ok {
			return false
		}
		if _, duplicate := seen[path]; duplicate {
			return false
		}
		seen[path] = struct{}{}
	}
	return true
}

type UADashboardImportLimits struct {
	HTTPBytes      int64
	ExtractedBytes int64
	FileBytes      int64
	Entries        int
	JSONDepth      int
}

func DefaultUADashboardImportLimits() UADashboardImportLimits {
	return UADashboardImportLimits{HTTPBytes: 64 << 20, ExtractedBytes: 256 << 20, FileBytes: 128 << 20, Entries: 1000, JSONDepth: 64}
}

type UADashboardManifest struct {
	SchemaVersion      int                       `json:"schemaVersion"`
	SourceCommitSHA    string                    `json:"sourceCommitSha"`
	DashboardCommitSHA string                    `json:"dashboardCommitSha"`
	Files              []UADashboardManifestFile `json:"files"`
}

type UADashboardManifestFile struct {
	Path    string `json:"path"`
	Bytes   int64  `json:"bytes"`
	BlobSHA string `json:"blobSha"`
}

func validateUADashboardJSON(data []byte, limits UADashboardImportLimits) error {
	if len(data) >= 3 && bytes.Equal(data[:3], []byte{0xef, 0xbb, 0xbf}) {
		return fmt.Errorf("%w: JSON byte-order marks are not supported", ErrUADashboardDataInvalid)
	}
	if !utf8.Valid(data) {
		return fmt.Errorf("%w: JSON must be valid UTF-8", ErrUADashboardDataInvalid)
	}
	depth := 0
	decoder := json.NewDecoder(bytes.NewReader(data))
	for {
		token, err := decoder.Token()
		if errors.Is(err, io.EOF) {
			break
		}
		if err != nil {
			return fmt.Errorf("%w: invalid JSON", ErrUADashboardDataInvalid)
		}
		if delimiter, ok := token.(json.Delim); ok {
			switch delimiter {
			case '{', '[':
				depth++
				if depth > limits.JSONDepth {
					return fmt.Errorf("%w: JSON nesting exceeds limit", ErrUADashboardArchiveLimit)
				}
			case '}', ']':
				depth--
			}
		}
	}
	valueDecoder := json.NewDecoder(bytes.NewReader(data))
	var value any
	if err := valueDecoder.Decode(&value); err != nil {
		return fmt.Errorf("%w: invalid JSON", ErrUADashboardDataInvalid)
	}
	if err := valueDecoder.Decode(&value); !errors.Is(err, io.EOF) {
		return fmt.Errorf("%w: JSON must contain one value", ErrUADashboardDataInvalid)
	}
	return nil
}
