package analysis

import (
	"context"
	"database/sql"
	"fmt"
	"strings"

	"github.com/dusk-network/pituitary/internal/config"
	"github.com/dusk-network/pituitary/internal/index"
	stindex "github.com/dusk-network/stroma/v3/index"
)

type analysisRepository struct {
	cfg            *config.Config
	ctx            context.Context
	db             *sql.DB
	snapshot       *stindex.Snapshot
	atDate         string // ISO date for point-in-time queries; empty = current
	minConfidence  string // minimum confidence tier filter; empty = all
	specCache      map[string]specDocument
	docCache       map[string]docDocument
	allSpecsLoaded bool
	allDocsLoaded  bool
}

func openAnalysisRepository(cfg *config.Config) (*analysisRepository, error) {
	return openAnalysisRepositoryContext(context.Background(), cfg)
}

func openAnalysisRepositoryContext(ctx context.Context, cfg *config.Config) (*analysisRepository, error) {
	if cfg == nil {
		return nil, fmt.Errorf("config is required")
	}

	db, err := index.OpenReadOnlyContext(ctx, cfg.Workspace.ResolvedIndexPath)
	if err != nil {
		return nil, fmt.Errorf("open index %s: %w", cfg.Workspace.ResolvedIndexPath, err)
	}

	snapshot, err := index.OpenStromaSnapshotContext(ctx, db, cfg.Workspace.ResolvedIndexPath)
	if err != nil {
		_ = db.Close()
		return nil, err
	}

	return &analysisRepository{
		cfg:      cfg,
		ctx:      ctx,
		db:       db,
		snapshot: snapshot,
	}, nil
}

func (r *analysisRepository) Close() error {
	if r == nil {
		return nil
	}
	var firstErr error
	if r.snapshot != nil {
		if err := r.snapshot.Close(); err != nil {
			firstErr = err
		}
	}
	if r.db != nil {
		if err := r.db.Close(); err != nil && firstErr == nil {
			firstErr = err
		}
	}
	return firstErr
}

func (r *analysisRepository) loadAllSpecs() (map[string]specDocument, error) {
	if !r.allSpecsLoaded {
		specs, err := loadIndexedSpecsContext(r.ctx, r.db, r.snapshot, nil)
		if err != nil {
			return nil, err
		}
		r.specCache = mergeSpecDocuments(r.specCache, specs)
		r.allSpecsLoaded = true
	}
	return copySpecDocuments(r.specCache), nil
}

func (r *analysisRepository) loadSpecs(refs []string) (map[string]specDocument, error) {
	refs = uniqueStrings(refs)
	if len(refs) == 0 {
		return r.loadAllSpecs()
	}
	if r.allSpecsLoaded {
		return selectSpecs(r.specCache, refs), nil
	}
	missing := missingSpecRefs(r.specCache, refs)
	if len(missing) > 0 {
		specs, err := loadIndexedSpecsContext(r.ctx, r.db, r.snapshot, missing)
		if err != nil {
			return nil, err
		}
		r.specCache = mergeSpecDocuments(r.specCache, specs)
	}
	return selectSpecs(r.specCache, refs), nil
}

func (r *analysisRepository) loadSelectedSpecs(refs []string) (map[string]specDocument, error) {
	refs = uniqueStrings(refs)
	if len(refs) == 0 {
		return map[string]specDocument{}, nil
	}
	return r.loadSpecs(refs)
}

func (r *analysisRepository) knownSpecRefs() ([]string, error) {
	rows, err := r.db.QueryContext(r.ctx, `
SELECT ref
FROM artifacts
WHERE kind = ?
ORDER BY ref ASC`, "spec")
	if err != nil {
		return nil, fmt.Errorf("query spec refs: %w", err)
	}
	defer rows.Close()

	refs := make([]string, 0)
	for rows.Next() {
		var ref string
		if err := rows.Scan(&ref); err != nil {
			return nil, fmt.Errorf("scan spec ref: %w", err)
		}
		refs = append(refs, ref)
	}
	if err := rows.Err(); err != nil {
		return nil, fmt.Errorf("iterate spec refs: %w", err)
	}
	return refs, nil
}

func (r *analysisRepository) loadAllDocs() (map[string]docDocument, error) {
	if !r.allDocsLoaded {
		docs, err := loadIndexedDocsContext(r.ctx, r.db, r.snapshot, nil)
		if err != nil {
			return nil, err
		}
		r.docCache = mergeDocDocuments(r.docCache, docs)
		r.allDocsLoaded = true
	}
	return copyDocDocuments(r.docCache), nil
}

func (r *analysisRepository) loadDocs(refs []string) (map[string]docDocument, error) {
	refs = uniqueStrings(refs)
	if len(refs) == 0 {
		return r.loadAllDocs()
	}
	if r.allDocsLoaded {
		return selectDocs(r.docCache, refs), nil
	}
	missing := missingDocRefs(r.docCache, refs)
	if len(missing) > 0 {
		docs, err := loadIndexedDocsContext(r.ctx, r.db, r.snapshot, missing)
		if err != nil {
			return nil, err
		}
		r.docCache = mergeDocDocuments(r.docCache, docs)
	}
	return selectDocs(r.docCache, refs), nil
}

func (r *analysisRepository) loadSelectedDocs(refs []string) (map[string]docDocument, error) {
	refs = uniqueStrings(refs)
	if len(refs) == 0 {
		return map[string]docDocument{}, nil
	}
	return r.loadDocs(refs)
}

func copySpecDocuments(source map[string]specDocument) map[string]specDocument {
	if len(source) == 0 {
		return map[string]specDocument{}
	}
	result := make(map[string]specDocument, len(source))
	for ref, document := range source {
		result[ref] = document
	}
	return result
}

func mergeSpecDocuments(destination, source map[string]specDocument) map[string]specDocument {
	if destination == nil {
		destination = map[string]specDocument{}
	}
	for ref, document := range source {
		destination[ref] = document
	}
	return destination
}

func copyDocDocuments(source map[string]docDocument) map[string]docDocument {
	if len(source) == 0 {
		return map[string]docDocument{}
	}
	result := make(map[string]docDocument, len(source))
	for ref, document := range source {
		result[ref] = document
	}
	return result
}

func mergeDocDocuments(destination, source map[string]docDocument) map[string]docDocument {
	if destination == nil {
		destination = map[string]docDocument{}
	}
	for ref, document := range source {
		destination[ref] = document
	}
	return destination
}

func selectSpecs(source map[string]specDocument, refs []string) map[string]specDocument {
	result := make(map[string]specDocument, len(refs))
	for _, ref := range refs {
		if document, ok := source[ref]; ok {
			result[ref] = document
		}
	}
	return result
}

func selectDocs(source map[string]docDocument, refs []string) map[string]docDocument {
	result := make(map[string]docDocument, len(refs))
	for _, ref := range refs {
		if document, ok := source[ref]; ok {
			result[ref] = document
		}
	}
	return result
}

func missingSpecRefs(source map[string]specDocument, refs []string) []string {
	if len(refs) == 0 {
		return nil
	}
	var missing []string
	for _, ref := range refs {
		if _, ok := source[ref]; !ok {
			missing = append(missing, ref)
		}
	}
	return missing
}

func missingDocRefs(source map[string]docDocument, refs []string) []string {
	if len(refs) == 0 {
		return nil
	}
	var missing []string
	for _, ref := range refs {
		if _, ok := source[ref]; !ok {
			missing = append(missing, ref)
		}
	}
	return missing
}

func artifactRepoID(metadata map[string]string) string {
	if len(metadata) == 0 {
		return ""
	}
	return strings.TrimSpace(metadata["repo_id"])
}
