package source

import (
	"context"
	"fmt"
	"path/filepath"
	"sort"
	"strings"

	"github.com/dusk-network/pituitary/internal/config"
	"github.com/dusk-network/pituitary/internal/diag"
	"github.com/dusk-network/pituitary/internal/model"
	"github.com/dusk-network/pituitary/sdk"
)

// LoadResult contains canonical records emitted by configured source adapters.
type LoadResult struct {
	Specs   []model.SpecRecord
	Docs    []model.DocRecord
	Sources []LoadSourceSummary
}

// LoadSourceSummary describes the records contributed by one configured source.
type LoadSourceSummary struct {
	Name      string `json:"name"`
	Repo      string `json:"repo,omitempty"`
	Adapter   string `json:"adapter"`
	Kind      string `json:"kind"`
	Path      string `json:"path"`
	ItemCount int    `json:"item_count"`
	SpecCount int    `json:"spec_count,omitempty"`
	DocCount  int    `json:"doc_count,omitempty"`
}

// LoadOptions controls diagnostic behavior while loading configured sources.
type LoadOptions struct {
	Logger *diag.Logger
}

// LoadFromConfig loads and normalizes all configured sources.
//
// This is a context-free convenience wrapper kept for callers that don't
// thread cancellation. It delegates to LoadFromConfigContext with a
// background context. Prefer LoadFromConfigContext on request paths so
// canceled CLI/MCP calls stop walking large workspaces promptly.
func LoadFromConfig(cfg *config.Config) (*LoadResult, error) {
	return LoadFromConfigContext(context.Background(), cfg)
}

// LoadFromConfigContext loads and normalizes all configured sources,
// passing ctx into adapter Load() so cancellation propagates into
// filesystem walks and bounded file reads.
func LoadFromConfigContext(ctx context.Context, cfg *config.Config) (*LoadResult, error) {
	return LoadFromConfigWithOptionsContext(ctx, cfg, LoadOptions{})
}

// LoadFromConfigWithOptions loads and normalizes all configured sources.
//
// Convenience wrapper for callers without a context; see
// LoadFromConfigWithOptionsContext for the cancellation-aware variant.
func LoadFromConfigWithOptions(cfg *config.Config, options LoadOptions) (*LoadResult, error) {
	return LoadFromConfigWithOptionsContext(context.Background(), cfg, options)
}

// LoadFromConfigWithOptionsContext loads and normalizes all configured
// sources with the supplied options, passing ctx into adapter Load().
func LoadFromConfigWithOptionsContext(ctx context.Context, cfg *config.Config, options LoadOptions) (*LoadResult, error) {
	if ctx == nil {
		ctx = context.Background()
	}
	logger := options.Logger
	result := &LoadResult{
		Sources: make([]LoadSourceSummary, 0, len(cfg.Sources)),
	}
	seenSpecs := make(map[string]artifactOrigin)
	seenDocs := make(map[string]artifactOrigin)
	logger.Infof("source", "loading %d configured source(s)", len(cfg.Sources))

	for _, source := range cfg.Sources {
		if err := ctx.Err(); err != nil {
			return nil, err
		}
		resolvedRepo := strings.TrimSpace(source.ResolvedRepo)
		if resolvedRepo == "" {
			resolvedRepo = config.PrimaryRepoID(cfg)
		}
		primaryRepoID := strings.TrimSpace(source.PrimaryRepo)
		if primaryRepoID == "" {
			primaryRepoID = config.PrimaryRepoID(cfg)
		}
		repoRootPath := strings.TrimSpace(source.RepoRootPath)
		if repoRootPath == "" {
			repoRootPath = cfg.Workspace.RootPath
		}
		logger.Debugf("source", "loading %s (%s %s)", sourceDisplayName(source), source.Kind, filepath.ToSlash(source.Path))
		summary := LoadSourceSummary{
			Name:    source.Name,
			Repo:    resolvedRepo,
			Adapter: source.Adapter,
			Kind:    source.Kind,
			Path:    source.Path,
		}

		factory := LookupAdapter(source.Adapter)
		if factory == nil {
			return nil, unknownAdapterError(source.Name, source.Adapter)
		}
		adapterResult, err := factory().Load(ctx, sdk.SourceConfig{
			Name:          source.Name,
			Adapter:       source.Adapter,
			Kind:          source.Kind,
			Repo:          resolvedRepo,
			Path:          source.Path,
			Files:         append([]string(nil), source.Files...),
			Include:       append([]string(nil), source.Include...),
			Exclude:       append([]string(nil), source.Exclude...),
			Options:       config.CloneSourceOptions(source.Options),
			WorkspaceRoot: repoRootPath,
			PrimaryRepoID: primaryRepoID,
		})
		if err != nil {
			return nil, fmt.Errorf("%s: %w", sourceDisplayName(source), err)
		}
		annotateSourceRoleMetadata(source.Role, adapterResult)
		annotateSourceRepoMetadata(resolvedRepo, adapterResult)
		annotateSourceAdapterMetadata(source.Adapter, adapterResult)

		if err := appendUniqueSpecRecords(result, seenSpecs, source, adapterResult.Specs); err != nil {
			return nil, err
		}
		if err := appendUniqueDocRecords(result, seenDocs, source, adapterResult.Docs); err != nil {
			return nil, err
		}
		summary.SpecCount = len(adapterResult.Specs)
		summary.DocCount = len(adapterResult.Docs)
		summary.ItemCount = len(adapterResult.Specs) + len(adapterResult.Docs)

		result.Sources = append(result.Sources, summary)
		if summary.ItemCount == 0 {
			logger.Warnf("source", "%s (%s %s) matched 0 item(s)", sourceDisplayName(source), source.Kind, filepath.ToSlash(source.Path))
			continue
		}
		logger.Infof(
			"source",
			"%s (%s %s) loaded %d item(s) (%d spec(s), %d doc(s))",
			sourceDisplayName(source),
			source.Kind,
			filepath.ToSlash(source.Path),
			summary.ItemCount,
			summary.SpecCount,
			summary.DocCount,
		)
	}

	sort.Slice(result.Specs, func(i, j int) bool {
		return result.Specs[i].Ref < result.Specs[j].Ref
	})
	sort.Slice(result.Docs, func(i, j int) bool {
		return result.Docs[i].Ref < result.Docs[j].Ref
	})

	return result, nil
}

func unknownAdapterError(sourceName, adapter string) error {
	registered := RegisteredAdapters()
	if len(registered) == 0 {
		return fmt.Errorf("source %q: unknown adapter %q", sourceName, adapter)
	}
	return fmt.Errorf(
		"source %q: unknown adapter %q (registered adapters: %s)",
		sourceName,
		adapter,
		strings.Join(registered, ", "),
	)
}

func annotateSourceRoleMetadata(role string, result *sdk.AdapterResult) {
	role = config.NormalizeSourceRole(role)
	if role == "" || result == nil {
		return
	}
	for i := range result.Specs {
		result.Specs[i].Metadata = withSourceRole(result.Specs[i].Metadata, role)
	}
	for i := range result.Docs {
		result.Docs[i].Metadata = withSourceRole(result.Docs[i].Metadata, role)
	}
}

func withSourceRole(metadata map[string]string, role string) map[string]string {
	if metadata == nil {
		metadata = map[string]string{}
	}
	metadata["source_role"] = role
	return metadata
}

func annotateSourceRepoMetadata(repoID string, result *sdk.AdapterResult) {
	repoID = strings.TrimSpace(repoID)
	if repoID == "" || result == nil {
		return
	}
	for i := range result.Specs {
		result.Specs[i].Metadata = withSourceRepo(result.Specs[i].Metadata, repoID)
	}
	for i := range result.Docs {
		result.Docs[i].Metadata = withSourceRepo(result.Docs[i].Metadata, repoID)
	}
}

func withSourceRepo(metadata map[string]string, repoID string) map[string]string {
	if metadata == nil {
		metadata = map[string]string{}
	}
	metadata["repo_id"] = repoID
	return metadata
}

func annotateSourceAdapterMetadata(adapter string, result *sdk.AdapterResult) {
	adapter = strings.TrimSpace(adapter)
	if adapter == "" || result == nil {
		return
	}
	for i := range result.Specs {
		result.Specs[i].Metadata = withSourceAdapter(result.Specs[i].Metadata, adapter)
	}
	for i := range result.Docs {
		result.Docs[i].Metadata = withSourceAdapter(result.Docs[i].Metadata, adapter)
	}
}

func withSourceAdapter(metadata map[string]string, adapter string) map[string]string {
	if metadata == nil {
		metadata = map[string]string{}
	}
	metadata["source_adapter"] = adapter
	return metadata
}

func sourceDisplayName(source config.Source) string {
	if strings.TrimSpace(source.Repo) != "" {
		return fmt.Sprintf("source %q repo %q", source.Name, source.ResolvedRepo)
	}
	return fmt.Sprintf("source %q", source.Name)
}
