package index

import (
	"context"
	"database/sql"
	"encoding/json"
	"errors"
	"fmt"
	"path/filepath"
	"runtime"
	"sort"
	"strings"

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

type indexedSpecPathRow struct {
	Ref          string
	SourceRef    string
	MetadataJSON string
}

type indexedSpecPathResolver struct {
	workspaceRoot string
	refsByPath    map[string][]string
}

var indexedSpecPathCaseInsensitive = runtime.GOOS == "windows"

type specPathNotFoundError struct {
	Input      string
	Normalized string
}

func (e *specPathNotFoundError) Error() string {
	if e.Normalized != "" && e.Normalized != e.Input {
		return fmt.Sprintf(
			`unknown --path %q (resolved to workspace path %q); run pituitary index --rebuild if the file should be indexed`,
			e.Input,
			e.Normalized,
		)
	}
	return fmt.Sprintf(`unknown --path %q; run pituitary index --rebuild if the file should be indexed`, e.Input)
}

type ambiguousSpecPathError struct {
	Input   string
	Matches []string
}

func (e *ambiguousSpecPathError) Error() string {
	return fmt.Sprintf(`ambiguous --path %q; matches indexed specs: %s`, e.Input, strings.Join(e.Matches, ", "))
}

type specPathOutsideWorkspaceError struct {
	Input         string
	WorkspaceRoot string
}

func (e *specPathOutsideWorkspaceError) Error() string {
	return fmt.Sprintf(`--path %q resolves outside workspace root %q`, e.Input, filepath.ToSlash(e.WorkspaceRoot))
}

// IsSpecPathNotFound reports whether err is a missing indexed-spec path lookup.
func IsSpecPathNotFound(err error) bool {
	var target *specPathNotFoundError
	return errors.As(err, &target)
}

// ResolveIndexedSpecRefWithConfigContext resolves one indexed spec ref from a path selector.
func ResolveIndexedSpecRefWithConfigContext(ctx context.Context, cfg *config.Config, rawPath string) (string, error) {
	refs, err := ResolveIndexedSpecRefsWithConfigContext(ctx, cfg, []string{rawPath})
	if err != nil {
		return "", err
	}
	if len(refs) != 1 {
		return "", fmt.Errorf("expected one resolved spec ref, got %d", len(refs))
	}
	return refs[0], nil
}

// ResolveIndexedSpecRefsWithConfigContext resolves indexed spec refs from workspace-relative or absolute paths.
func ResolveIndexedSpecRefsWithConfigContext(ctx context.Context, cfg *config.Config, rawPaths []string) ([]string, error) {
	resolver, err := newIndexedSpecPathResolverContext(ctx, cfg)
	if err != nil {
		return nil, err
	}

	refs := make([]string, 0, len(rawPaths))
	for _, rawPath := range rawPaths {
		ref, err := resolver.Resolve(rawPath)
		if err != nil {
			return nil, err
		}
		refs = append(refs, ref)
	}
	return refs, nil
}

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

	db, err := OpenReadOnlyContext(ctx, cfg.Workspace.ResolvedIndexPath)
	if err != nil {
		return nil, err
	}
	defer db.Close()

	rows, err := loadIndexedSpecPathRowsContext(ctx, db)
	if err != nil {
		return nil, err
	}

	refsByPath := make(map[string][]string, len(rows)*2)
	for _, row := range rows {
		candidatePaths, err := indexedSpecCandidatePaths(row)
		if err != nil {
			return nil, err
		}
		for _, candidatePath := range candidatePaths {
			if candidatePath == "" {
				continue
			}
			refsByPath[candidatePath] = appendUniqueSorted(refsByPath[candidatePath], row.Ref)
		}
	}

	return &indexedSpecPathResolver{
		workspaceRoot: cfg.Workspace.RootPath,
		refsByPath:    refsByPath,
	}, nil
}

func loadIndexedSpecPathRowsContext(ctx context.Context, db *sql.DB) ([]indexedSpecPathRow, error) {
	rows, err := db.QueryContext(ctx, `
SELECT ref, source_ref, metadata_json
FROM artifacts
WHERE kind = ?
ORDER BY ref ASC`, model.ArtifactKindSpec)
	if err != nil {
		return nil, fmt.Errorf("query indexed spec paths: %w", err)
	}
	defer rows.Close()

	var result []indexedSpecPathRow
	for rows.Next() {
		var row indexedSpecPathRow
		if err := rows.Scan(&row.Ref, &row.SourceRef, &row.MetadataJSON); err != nil {
			return nil, fmt.Errorf("scan indexed spec path: %w", err)
		}
		result = append(result, row)
	}
	if err := rows.Err(); err != nil {
		return nil, fmt.Errorf("iterate indexed spec paths: %w", err)
	}
	return result, nil
}

func indexedSpecCandidatePaths(row indexedSpecPathRow) ([]string, error) {
	candidates := []string{
		normalizeIndexedSpecLookupPath(row.SourceRef),
		normalizeIndexedSpecLookupPath(strings.TrimPrefix(strings.TrimSpace(row.SourceRef), "file://")),
	}

	var metadata map[string]string
	if strings.TrimSpace(row.MetadataJSON) != "" {
		if err := json.Unmarshal([]byte(row.MetadataJSON), &metadata); err != nil {
			return nil, fmt.Errorf("parse indexed metadata for %s: %w", row.Ref, err)
		}
	}
	for _, key := range []string{"path", "body_path", "bundle_path"} {
		if value := normalizeIndexedSpecLookupPath(metadata[key]); value != "" {
			candidates = append(candidates, value)
		}
	}
	return uniqueSortedStrings(candidates), nil
}

func (r *indexedSpecPathResolver) Resolve(rawPath string) (string, error) {
	normalized, err := normalizeSpecSelectorPath(r.workspaceRoot, rawPath)
	if err != nil {
		return "", err
	}

	matches := append([]string(nil), r.refsByPath[normalized]...)
	switch len(matches) {
	case 0:
		return "", &specPathNotFoundError{
			Input:      strings.TrimSpace(rawPath),
			Normalized: normalized,
		}
	case 1:
		return matches[0], nil
	default:
		return "", &ambiguousSpecPathError{
			Input:   strings.TrimSpace(rawPath),
			Matches: matches,
		}
	}
}

func normalizeSpecSelectorPath(workspaceRoot, rawPath string) (string, error) {
	rawPath = strings.TrimSpace(rawPath)
	if rawPath == "" {
		return "", fmt.Errorf("--path must not be empty")
	}

	absPath := rawPath
	if !filepath.IsAbs(absPath) {
		absPath = filepath.Join(workspaceRoot, absPath)
	}
	absPath, err := filepath.Abs(absPath)
	if err != nil {
		return "", fmt.Errorf("resolve path %q: %w", rawPath, err)
	}

	relPath, err := filepath.Rel(workspaceRoot, absPath)
	if err != nil {
		return "", fmt.Errorf("resolve workspace-relative path for %q: %w", rawPath, err)
	}
	if pathEscapesRoot(relPath) {
		return "", &specPathOutsideWorkspaceError{
			Input:         rawPath,
			WorkspaceRoot: workspaceRoot,
		}
	}
	return normalizeIndexedSpecLookupPath(relPath), nil
}

func normalizeIndexedSpecPath(path string) string {
	path = strings.TrimSpace(path)
	if path == "" {
		return ""
	}
	return filepath.ToSlash(filepath.Clean(path))
}

func normalizeIndexedSpecLookupPath(path string) string {
	path = normalizeIndexedSpecPath(path)
	if path == "" {
		return ""
	}
	if indexedSpecPathCaseInsensitive {
		return strings.ToLower(path)
	}
	return path
}

func pathEscapesRoot(relPath string) bool {
	relPath = filepath.Clean(relPath)
	if relPath == ".." {
		return true
	}
	return strings.HasPrefix(relPath, ".."+string(filepath.Separator))
}

func appendUniqueSorted(values []string, value string) []string {
	for _, existing := range values {
		if existing == value {
			return values
		}
	}
	values = append(values, value)
	sort.Strings(values)
	return values
}

func uniqueSortedStrings(values []string) []string {
	if len(values) == 0 {
		return nil
	}
	result := make([]string, 0, len(values))
	for _, value := range values {
		if value == "" {
			continue
		}
		result = appendUniqueSorted(result, value)
	}
	return result
}
