package validation

import (
	_ "embed"
	"encoding/json"
	"fmt"
	"sync"

	"github.com/klauspost/compress/zstd"
	"github.com/xeipuuv/gojsonschema"

	"{{ module_path }}/internal/core"
)

// Each known version's generated-schemas asset is written directly by the
// Rust generator (not templated) and baked into the compiled binary via
// its own go:embed directive rather than read from disk at runtime —
// mirrors `targets::rust::templates::validation::validator.rs`'s
// `include_bytes!` approach, keeping this file's size independent of how
// many operations the source spec declares. A `go:embed` directive binds
// to exactly one var, so there's one embed+var pair per version here
// rather than a single shared one.
//
// The asset is zstd-compressed: every operation's schema embeds a full
// copy of the spec's `$defs` library (a deliberate generator tradeoff —
// see `mcpify`'s `openapi::schema_resolve` — simpler than cross-schema
// `$ref` resolution, at the cost of file size), and a long-distance-
// matching compressor collapses that duplication (measured ~190 MB down
// to tens of KB on a real spec) without requiring any change to the JSON
// Schema shape itself. Decompressed once per version, lazily, in
// schemasByOperation below.
// mcpify:versions:begin
{% for v in version_entries %}
//go:embed {{ v.schemas_file }}
var embeddedSchemas_{{ v.var_suffix }} []byte
{% endfor %}
var generatedSchemasByVersion = map[string][]byte{
{%- for v in version_entries %}
	"{{ v.label }}": embeddedSchemas_{{ v.var_suffix }},
{%- endfor %}
}

// mcpify:versions:end

type operationSchemas struct {
	InputSchema  json.RawMessage `json:"inputSchema"`
	OutputSchema json.RawMessage `json:"outputSchema"`
}

var schemasByVersionMu sync.Mutex
var schemasByVersion = map[string]map[string]operationSchemas{}

var validatorCacheMu sync.Mutex
var validatorCache = map[string]*gojsonschema.Schema{}

func schemasByOperation(apiVersion string) map[string]operationSchemas {
	schemasByVersionMu.Lock()
	defer schemasByVersionMu.Unlock()

	if cached, ok := schemasByVersion[apiVersion]; ok {
		return cached
	}

	parsed := map[string]operationSchemas{}
	if compressed, ok := generatedSchemasByVersion[apiVersion]; ok {
		if decoder, err := zstd.NewReader(nil); err == nil {
			if raw, err := decoder.DecodeAll(compressed, nil); err == nil {
				_ = json.Unmarshal(raw, &parsed)
			}
			decoder.Close()
		}
	}
	schemasByVersion[apiVersion] = parsed
	return parsed
}

func validatorFor(apiVersion, operationID, schemaKey string) (*gojsonschema.Schema, error) {
	cacheKey := apiVersion + "\x00" + operationID + "\x00" + schemaKey
	validatorCacheMu.Lock()
	defer validatorCacheMu.Unlock()

	if cached, ok := validatorCache[cacheKey]; ok {
		return cached, nil
	}

	entry := schemasByOperation(apiVersion)[operationID]
	var raw json.RawMessage
	if schemaKey == "inputSchema" {
		raw = entry.InputSchema
	} else {
		raw = entry.OutputSchema
	}
	if len(raw) == 0 {
		raw = json.RawMessage("{}")
	}

	schema, err := gojsonschema.NewSchema(gojsonschema.NewBytesLoader(raw))
	if err != nil {
		return nil, err
	}
	validatorCache[cacheKey] = schema
	return schema, nil
}

func validateAgainst(apiVersion, operationID, schemaKey string, data any) ([]string, error) {
	schema, err := validatorFor(apiVersion, operationID, schemaKey)
	if err != nil {
		return nil, err
	}
	result, err := schema.Validate(gojsonschema.NewGoLoader(data))
	if err != nil {
		return nil, err
	}
	if result.Valid() {
		return nil, nil
	}
	messages := make([]string, len(result.Errors()))
	for i, e := range result.Errors() {
		messages[i] = e.String()
	}
	return messages, nil
}

// ValidateInput raises a ValidationError if data doesn't satisfy
// operationID's input schema, for the given apiVersion.
func ValidateInput(apiVersion, operationID string, data any) error {
	errors, err := validateAgainst(apiVersion, operationID, "inputSchema", data)
	if err != nil {
		return err
	}
	if len(errors) > 0 {
		return core.NewValidationError(fmt.Sprintf("invalid input for '%s'", operationID), errors)
	}
	return nil
}

// ValidateOutput raises a ValidationError if data doesn't satisfy
// operationID's output schema — surfaces upstream API drift as a
// structured error rather than silently returning a mismatched response
// (architecture.md's call pipeline). Validates against the given
// apiVersion's schema.
func ValidateOutput(apiVersion, operationID string, data any) error {
	errors, err := validateAgainst(apiVersion, operationID, "outputSchema", data)
	if err != nil {
		return err
	}
	if len(errors) > 0 {
		return core.NewValidationError(fmt.Sprintf("unexpected response shape for '%s'", operationID), errors)
	}
	return nil
}
