package runtime

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

	agsserver "github.com/pingcap/agent-git-service/server"
)

const (
	agsRepositoryRefPrefix = "ags-repo:"
	agsWikiRefPrefix       = "ags-wiki:"
)

// AGSRepositorySnapshotServer is the trusted in-process subset exposed by the
// embedded AGS server. It never accepts filesystem paths.
type AGSRepositorySnapshotServer interface {
	GrepRepositorySnapshot(context.Context, agsserver.RepositorySnapshotGrepRequest, func(agsserver.RepositorySnapshotGrepMatch) error) error
	ListRepositorySnapshot(context.Context, agsserver.RepositorySnapshotListRequest, func(agsserver.RepositorySnapshotListEntry) error) error
	ReadRepositorySnapshot(context.Context, agsserver.RepositorySnapshotReadRequest, func(agsserver.RepositorySnapshotReadChunk) error) error
	ResolveRepositorySnapshotRef(context.Context, agsserver.RepositorySnapshotRefRequest) (string, error)
}

type AGSRepositoryPrimitive struct {
	Server AGSRepositorySnapshotServer
}

// NewAGSRepositoryRef creates the server-only repository address stored in an
// authorized root handle. Wiki-backed lexicon/insights roots address AGS's
// companion .wiki Git repository; primary roots address the repository itself.
func NewAGSRepositoryRef(repositoryFullName, rootKind, repoRole string) (string, error) {
	fullName, err := normalizeAGSRepositoryFullName(repositoryFullName)
	if err != nil {
		return "", err
	}
	if !validRootKindRole(rootKind, repoRole) {
		return "", fmt.Errorf("knowledge root kind/repository role is invalid")
	}
	switch strings.TrimSpace(rootKind) {
	case RootKindLexicon, RootKindInsights:
		return agsWikiRefPrefix + fullName, nil
	case RootKindPrimary:
		return agsRepositoryRefPrefix + fullName, nil
	default:
		return "", fmt.Errorf("knowledge root kind is invalid")
	}
}

// Grep projects the server-only snapshot address into AGS's exact-commit
// content search primitive.
func (primitive AGSRepositoryPrimitive) Grep(ctx context.Context, snapshot RepositorySnapshot, request RepositoryGrepRequest, emit func(RepositoryGrepMatch) error) error {
	if primitive.Server == nil {
		return repositoryFailure("AGS repository snapshot service is unavailable", nil)
	}
	fullName, err := resolveAGSRepositoryRef(snapshot.RepositoryRef)
	if err != nil {
		return ErrRepositoryEscape
	}
	err = primitive.Server.GrepRepositorySnapshot(ctx, agsserver.RepositorySnapshotGrepRequest{
		RepositoryFullName: fullName,
		CommitSHA:          strings.TrimSpace(snapshot.Revision),
		Pattern:            request.Pattern,
		PathGlobs:          append([]string(nil), request.PathGlobs...),
	}, func(match agsserver.RepositorySnapshotGrepMatch) error {
		return emit(RepositoryGrepMatch{
			Path:    match.Path,
			Line:    match.Line,
			Preview: match.Preview,
		})
	})
	return mapAGSRepositoryError(err)
}

// Read projects the server-only snapshot address into AGS's exact-range read
// primitive.
func (primitive AGSRepositoryPrimitive) Read(ctx context.Context, snapshot RepositorySnapshot, request RepositoryReadRequest, emit func(RepositoryReadChunk) error) error {
	if primitive.Server == nil {
		return repositoryFailure("AGS repository snapshot service is unavailable", nil)
	}
	fullName, err := resolveAGSRepositoryRef(snapshot.RepositoryRef)
	if err != nil {
		return ErrRepositoryEscape
	}
	err = primitive.Server.ReadRepositorySnapshot(ctx, agsserver.RepositorySnapshotReadRequest{
		RepositoryFullName: fullName,
		CommitSHA:          strings.TrimSpace(snapshot.Revision),
		Path:               request.Path,
		LineStart:          request.LineStart,
		LineEnd:            request.LineEnd,
	}, func(chunk agsserver.RepositorySnapshotReadChunk) error {
		return emit(RepositoryReadChunk{
			Content:   append([]byte(nil), chunk.Content...),
			LineStart: chunk.LineStart,
			LineEnd:   chunk.LineEnd,
		})
	})
	return mapAGSRepositoryError(err)
}

// List projects the server-only snapshot address into AGS's exact-commit tree
// listing primitive.
func (primitive AGSRepositoryPrimitive) List(ctx context.Context, snapshot RepositorySnapshot, request RepositoryListRequest, emit func(RepositoryListEntry) error) error {
	if primitive.Server == nil {
		return repositoryFailure("AGS repository snapshot service is unavailable", nil)
	}
	fullName, err := resolveAGSRepositoryRef(snapshot.RepositoryRef)
	if err != nil {
		return ErrRepositoryEscape
	}
	err = primitive.Server.ListRepositorySnapshot(ctx, agsserver.RepositorySnapshotListRequest{
		RepositoryFullName: fullName,
		CommitSHA:          strings.TrimSpace(snapshot.Revision),
		Path:               request.Path,
	}, func(entry agsserver.RepositorySnapshotListEntry) error {
		return emit(RepositoryListEntry{Path: entry.Path, Kind: entry.Kind})
	})
	return mapAGSRepositoryError(err)
}

// ResolveRef binds one explicit branch or tag in the same server-authorized
// repository to an immutable commit. Ref names never flow into the exact-commit
// content primitives after this resolution step.
func (primitive AGSRepositoryPrimitive) ResolveRef(ctx context.Context, snapshot RepositorySnapshot, refName, refKind string) (string, error) {
	if primitive.Server == nil {
		return "", repositoryFailure("AGS repository snapshot service is unavailable", nil)
	}
	fullName, err := resolveAGSRepositoryRef(snapshot.RepositoryRef)
	if err != nil {
		return "", ErrRepositoryEscape
	}
	sha, err := primitive.Server.ResolveRepositorySnapshotRef(ctx, agsserver.RepositorySnapshotRefRequest{
		RepositoryFullName: fullName,
		RefName:            strings.TrimSpace(refName),
		RefType:            strings.TrimSpace(refKind),
	})
	if errors.Is(err, agsserver.ErrRepositorySnapshotRefNotFound) {
		return "", ErrRepositoryRefNotFound
	}
	if errors.Is(err, agsserver.ErrRepositorySnapshotRefInvalid) {
		return "", ErrRepositoryRefInvalid
	}
	if err != nil {
		return "", mapAGSRepositoryError(err)
	}
	return strings.TrimSpace(sha), nil
}

func resolveAGSRepositoryRef(reference string) (string, error) {
	reference = strings.TrimSpace(reference)
	switch {
	case strings.HasPrefix(reference, agsRepositoryRefPrefix):
		return normalizeAGSRepositoryFullName(strings.TrimPrefix(reference, agsRepositoryRefPrefix))
	case strings.HasPrefix(reference, agsWikiRefPrefix):
		fullName, err := normalizeAGSRepositoryFullName(strings.TrimPrefix(reference, agsWikiRefPrefix))
		if err != nil {
			return "", err
		}
		return fullName + ".wiki", nil
	default:
		return "", fmt.Errorf("AGS repository reference is invalid")
	}
}

func normalizeAGSRepositoryFullName(raw string) (string, error) {
	raw = strings.TrimSpace(raw)
	owner, name, ok := strings.Cut(raw, "/")
	if !ok || strings.TrimSpace(owner) == "" || strings.TrimSpace(name) == "" ||
		strings.Contains(name, "/") || strings.ContainsAny(raw, `\`+"\x00") ||
		owner == "." || owner == ".." || name == "." || name == ".." {
		return "", fmt.Errorf("AGS repository full name is invalid")
	}
	return owner + "/" + name, nil
}

func mapAGSRepositoryError(err error) error {
	if err == nil {
		return nil
	}
	if errors.Is(err, agsserver.ErrRepositorySnapshotEscape) {
		return ErrRepositoryEscape
	}
	if errors.Is(err, agsserver.ErrRepositorySnapshotResourceExhausted) {
		return ErrRepositoryExhausted
	}
	return err
}
