package exec

import (
	"context"
	"errors"
	"fmt"
	"strings"

	"github.com/tidbcloud/AgentTeam/desktop/internal/wire/apiclient"
)

type extensionRootResponse struct {
	Extensions *[]extensionCatalogEntry `json:"extensions"`
}

type extensionCatalogEntry struct {
	ExtensionName string                   `json:"extensionName"`
	DisplayName   string                   `json:"displayName"`
	Actions       []extensionActionCatalog `json:"actions"`
}

type extensionActionCatalog struct {
	Name         string          `json:"name"`
	Summary      string          `json:"summary"`
	Description  string          `json:"description"`
	Safety       ExtensionSafety `json:"safety"`
	InputSchema  map[string]any  `json:"inputSchema"`
	OutputSchema map[string]any  `json:"outputSchema"`
	Example      any             `json:"example"`
}

const maxExtensionCatalogDiagnostics = 32

// FetchExtensionSnapshot conditionally fetches the complete authoritative
// Catalog document in one request. A 304 reuses cached only when cached carries
// the exact returned ETag. On a 200, malformed Extensions or Actions are
// isolated independently with bounded diagnostics so one bad unit cannot keep
// the remaining Extensions (or the agent) from starting.
func FetchExtensionSnapshot(
	ctx context.Context,
	client *apiclient.Client,
	cached ExtensionSnapshot,
) (snapshot ExtensionSnapshot, diagnostics []string, modified bool, err error) {
	if client == nil || client.HTTPClient == nil {
		return ExtensionSnapshot{}, nil, false,
			errors.New("fetch Extension Catalog: API client unavailable")
	}
	if !exactIdentity(client.AgentID) ||
		!exactIdentity(client.RouteWorkspaceID) {
		return ExtensionSnapshot{}, nil, false, errors.New(
			"fetch Extension Catalog: invalid agent or workspace identity",
		)
	}

	var root extensionRootResponse
	response, err := client.GetConditional(
		ctx,
		client.AgentRuntimePath("/ext"),
		cached.ETag(),
		&root,
	)
	if err != nil {
		return ExtensionSnapshot{}, nil, false,
			fmt.Errorf("fetch Extension Catalog root: %w", err)
	}
	if response.NotModified {
		if cached.ETag() == "" || response.ETag != cached.ETag() {
			return ExtensionSnapshot{}, nil, false, errors.New(
				"fetch Extension Catalog: 304 without the complete matching cache",
			)
		}
		return cached, nil, false, nil
	}
	if !validCatalogETag(response.ETag) {
		return ExtensionSnapshot{}, nil, false, errors.New(
			"fetch Extension Catalog: response ETag is missing or invalid",
		)
	}
	if root.Extensions == nil {
		return ExtensionSnapshot{}, nil, false, errors.New(
			"fetch Extension Catalog: extensions document is missing",
		)
	}

	domains, diagnostics := projectFetchedExtensions(*root.Extensions)
	if len(*root.Extensions) > 0 && len(domains) == 0 {
		return ExtensionSnapshot{}, diagnostics, false, fmt.Errorf(
			"fetch Extension Catalog: every Extension was skipped (%d diagnostics)",
			len(diagnostics),
		)
	}
	snapshot = NewExtensionSnapshot(response.ETag, domains)
	if err := ValidateExtensionSnapshot(snapshot); err != nil {
		return ExtensionSnapshot{}, diagnostics, false,
			fmt.Errorf("fetch Extension Catalog: %w", err)
	}
	return snapshot, diagnostics, true, nil
}

func projectFetchedExtensions(
	entries []extensionCatalogEntry,
) ([]ExtensionDomain, []string) {
	domains := make([]ExtensionDomain, 0, len(entries))
	collector := extensionDiagnosticCollector{}
	seenExtensions := make(map[string]struct{}, len(entries))
	for _, entry := range entries {
		domain, unitDiagnostics := projectFetchedExtension(
			entry,
			seenExtensions,
		)
		for _, diagnostic := range unitDiagnostics {
			collector.Add(diagnostic)
		}
		if len(domain.Actions) == 0 {
			continue
		}
		domains = append(domains, domain)
	}
	return domains, collector.List()
}

func projectFetchedExtension(
	entry extensionCatalogEntry,
	seenExtensions map[string]struct{},
) (ExtensionDomain, []string) {
	if err := validateFetchedExtension(
		entry,
		seenExtensions,
	); err != nil {
		return ExtensionDomain{}, []string{
			extensionDomainDiagnostic(entry.ExtensionName, err),
		}
	}
	domain := ExtensionDomain{
		ExtensionName: entry.ExtensionName,
		DisplayName:   entry.DisplayName,
		Actions:       make([]ExtensionAction, 0, len(entry.Actions)),
	}
	collector := extensionDiagnosticCollector{}
	seenActions := make(map[string]struct{}, len(entry.Actions))
	for _, descriptor := range entry.Actions {
		action, err := projectFetchedAction(entry, descriptor, seenActions)
		if err != nil {
			collector.Add(extensionActionDiagnostic(
				entry.ExtensionName,
				descriptor.Name,
				"fetch",
				err,
			))
			continue
		}
		domain.Actions = append(domain.Actions, action)
	}
	if len(domain.Actions) == 0 {
		collector.Add(extensionDomainDiagnostic(
			entry.ExtensionName,
			errors.New("every Action was skipped; Extension excluded"),
		))
	}
	return domain, collector.List()
}

func validateFetchedExtension(
	entry extensionCatalogEntry,
	seen map[string]struct{},
) error {
	if !exactIdentity(entry.ExtensionName) ||
		strings.TrimSpace(entry.DisplayName) == "" ||
		entry.DisplayName != strings.TrimSpace(entry.DisplayName) {
		return errors.New("fetch Extension Catalog: invalid Extension identity")
	}
	if err := validateName(entry.ExtensionName); err != nil {
		return fmt.Errorf(
			"fetch Extension Catalog: invalid Extension name: %w",
			err,
		)
	}
	if _, duplicate := seen[entry.ExtensionName]; duplicate {
		return fmt.Errorf(
			"fetch Extension Catalog: duplicate Extension name %q",
			entry.ExtensionName,
		)
	}
	seen[entry.ExtensionName] = struct{}{}
	if len(entry.Actions) == 0 {
		return fmt.Errorf(
			"fetch Extension Catalog: Extension name %q has no authorized Actions",
			entry.ExtensionName,
		)
	}
	return nil
}

func projectFetchedAction(
	entry extensionCatalogEntry,
	descriptor extensionActionCatalog,
	seen map[string]struct{},
) (ExtensionAction, error) {
	if !exactIdentity(descriptor.Name) ||
		!exactIdentity(descriptor.Safety.Class) {
		return ExtensionAction{}, errors.New(
			"fetch Extension Catalog: invalid Action identity",
		)
	}
	if err := validateName(descriptor.Name); err != nil {
		return ExtensionAction{}, fmt.Errorf(
			"fetch Extension Catalog: invalid Action name: %w",
			err,
		)
	}
	switch descriptor.Safety.Class {
	case "read", "mutation", "destructive":
	default:
		return ExtensionAction{}, fmt.Errorf(
			"fetch Extension Catalog: invalid Action safety class %q",
			descriptor.Safety.Class,
		)
	}
	if _, duplicate := seen[descriptor.Name]; duplicate {
		return ExtensionAction{}, fmt.Errorf(
			"fetch Extension Catalog: duplicate Action %q",
			descriptor.Name,
		)
	}
	seen[descriptor.Name] = struct{}{}
	if descriptor.InputSchema == nil || descriptor.OutputSchema == nil {
		return ExtensionAction{}, errors.New(
			"fetch Extension Catalog: input/output schema is required",
		)
	}
	inputValidator, err := compileExtensionSchema(descriptor.InputSchema)
	if err != nil {
		return ExtensionAction{}, fmt.Errorf(
			"fetch Extension Catalog: compile input schema: %w",
			err,
		)
	}
	return ExtensionAction{
		ExtensionName:  entry.ExtensionName,
		DisplayName:    entry.DisplayName,
		Name:           descriptor.Name,
		Summary:        descriptor.Summary,
		Description:    descriptor.Description,
		Safety:         descriptor.Safety,
		InputSchema:    descriptor.InputSchema,
		OutputSchema:   descriptor.OutputSchema,
		Example:        descriptor.Example,
		inputValidator: inputValidator,
	}, nil
}

func validCatalogETag(value string) bool {
	return len(value) >= 2 &&
		value[0] == '"' &&
		value[len(value)-1] == '"' &&
		value == strings.TrimSpace(value) &&
		!strings.ContainsAny(value, "\r\n,")
}

func exactIdentity(value string) bool {
	return value != "" && value == strings.TrimSpace(value)
}

type extensionDiagnosticCollector struct {
	items   []string
	omitted int
}

func (collector *extensionDiagnosticCollector) Add(value string) {
	if collector == nil || strings.TrimSpace(value) == "" {
		return
	}
	if len(collector.items) < maxExtensionCatalogDiagnostics {
		collector.items = append(collector.items, value)
		return
	}
	if collector.omitted == 0 {
		// Reserve the final bounded slot for the omission summary. The
		// diagnostic previously occupying it and this new diagnostic are both
		// omitted.
		collector.omitted = 2
	} else {
		collector.omitted++
	}
	collector.items[maxExtensionCatalogDiagnostics-1] = fmt.Sprintf(
		"extension=* action=- schema=catalog: %d additional diagnostics omitted",
		collector.omitted,
	)
}

func (collector extensionDiagnosticCollector) List() []string {
	return append([]string(nil), collector.items...)
}
