package skills

import (
	"bytes"
	"context"
	"crypto/sha256"
	"encoding/hex"
	"errors"
	"fmt"
	"io"
	"io/fs"
	"os"
	"path/filepath"
	"sort"
	"strings"
	"time"

	"github.com/tidbcloud/AgentTeam/internal/contentpayload"
	contentservice "github.com/tidbcloud/AgentTeam/internal/service/content"
)

const (
	skillContentDomain           = "skill"
	skillContentMaxFiles         = maxSkillArchiveEntries
	skillContentMaxFileBytes     = maxSkillArchiveEntryBytes
	skillContentMaxTotalBytes    = maxSkillArchiveExtractedBytes
	skillContentMaxManifestBytes = int64(8 << 20)
	skillContentOperationTimeout = 2 * time.Minute
)

type nativeSkillContentService interface {
	FindContentByBinding(context.Context, string, string, string) (contentservice.Content, bool, error)
	CreateContent(context.Context, contentservice.CreateContentRequest) (contentservice.Content, error)
	CommitVersion(context.Context, contentservice.CommitVersionRequest) (contentservice.CommitVersionResult, error)
	FindVersionOperation(context.Context, string, string, string) (contentservice.VersionOperationResult, bool, error)
	MaterializeTree(context.Context, string, string, string, string, contentpayload.MaterializeLimits) error
	OpenFile(context.Context, string, string, string, string, *contentpayload.ByteRange) (io.ReadCloser, contentpayload.BlobMeta, error)
	CreatePin(context.Context, contentservice.CreatePinRequest) (contentservice.Pin, error)
	ListPins(context.Context, string, string, int) ([]contentservice.Pin, error)
}

type NativeSkillContentRequest struct {
	WorkspaceID string
	SkillID     string
	ContentID   string
	VersionID   string
	Actor       string
}

type NativeSkillInitializeRequest struct {
	WorkspaceID string
	SkillID     string
	Actor       string
	Slug        string
	Name        string
	Description string
	Locale      string
}

type NativeSkillVersionResult struct {
	ContentID      string
	VersionID      string
	RootDigest     string
	FileCount      int
	SizeBytes      int64
	SkillCount     int
	Preview        SkillRepositoryScanData
	ImportedSkills []SkillRepositoryDetectedSkill
	Warnings       []SkillImportWarning
}

type NativeSkillContent interface {
	Initialize(context.Context, NativeSkillInitializeRequest) (NativeSkillVersionResult, error)
	List(context.Context, NativeSkillContentRequest) (SkillRepositoryScanData, error)
	Read(context.Context, NativeSkillContentRequest, SkillRepositoryContentsRequest) (SkillRepositoryContents, error)
	Import(context.Context, NativeSkillContentRequest, SkillContentImportMutation) (NativeSkillVersionResult, error)
	Restore(context.Context, NativeSkillContentRequest, string) (NativeSkillVersionResult, error)
	CreateBackup(context.Context, NativeSkillContentRequest, string) (contentservice.Pin, error)
	ListBackups(context.Context, NativeSkillContentRequest, int) ([]contentservice.Pin, error)
}

// NativeSkillContentStore owns the exact tree mapping for Skill collections.
// It never accepts a repository identity and therefore cannot silently clone,
// push, or restore through AGS.
type NativeSkillContentStore struct{ Content nativeSkillContentService }

func (store NativeSkillContentStore) Initialize(ctx context.Context, request NativeSkillInitializeRequest) (NativeSkillVersionResult, error) {
	ctx, cancel := boundedSkillContentContext(ctx)
	defer cancel()
	if store.Content == nil || strings.TrimSpace(request.WorkspaceID) == "" || strings.TrimSpace(request.SkillID) == "" || strings.TrimSpace(request.Actor) == "" {
		return NativeSkillVersionResult{}, invalidArgument("native skill content is not configured")
	}
	root, err := os.MkdirTemp("", "meshx-skill-native-init-*")
	if err != nil {
		return NativeSkillVersionResult{}, err
	}
	defer os.RemoveAll(root)
	files := skillTemplateFiles(request.Slug, request.Name, request.Description, request.Locale)
	if len(files) == 0 {
		return NativeSkillVersionResult{}, fmt.Errorf("load skill scaffold")
	}
	for _, file := range files {
		if err := writeNativeSkillFile(root, file.path, []byte(file.content)); err != nil {
			return NativeSkillVersionResult{}, err
		}
	}
	content, err := store.findOrCreateContent(ctx, request.WorkspaceID, request.SkillID, request.Actor)
	if err != nil {
		return NativeSkillVersionResult{}, err
	}
	return store.publishDirectory(ctx, NativeSkillContentRequest{
		WorkspaceID: request.WorkspaceID, SkillID: request.SkillID, ContentID: content.ID,
		VersionID: content.HeadVersionID, Actor: request.Actor,
	}, content, root, "initialize")
}

func (store NativeSkillContentStore) List(ctx context.Context, request NativeSkillContentRequest) (SkillRepositoryScanData, error) {
	ctx, cancel := boundedSkillContentContext(ctx)
	defer cancel()
	root, cleanup, err := store.materialize(ctx, request, "meshx-skill-native-list-*")
	if err != nil {
		return SkillRepositoryScanData{}, err
	}
	defer cleanup()
	preview, err := ScanSkillRepository(root, SkillRepositoryScanLimit)
	if err != nil {
		return SkillRepositoryScanData{}, err
	}
	preview.VersionID = strings.TrimSpace(request.VersionID)
	return preview, nil
}

func (store NativeSkillContentStore) Read(ctx context.Context, request NativeSkillContentRequest, input SkillRepositoryContentsRequest) (SkillRepositoryContents, error) {
	ctx, cancel := boundedSkillContentContext(ctx)
	defer cancel()
	if err := validateNativeSkillContentRequest(request); err != nil {
		return SkillRepositoryContents{}, err
	}
	expected := strings.TrimSpace(input.ExpectedVersionID)
	if expected == "" || expected != strings.TrimSpace(request.VersionID) {
		return SkillRepositoryContents{}, ErrSkillRepositorySnapshotChanged
	}
	paths, err := normalizeSkillRepositoryContentPaths(input.Paths)
	if err != nil {
		return SkillRepositoryContents{}, err
	}
	items := make([]SkillRepositoryContent, 0, len(paths))
	for _, skillPath := range paths {
		if !strings.EqualFold(filepath.Base(filepath.FromSlash(skillPath)), "SKILL.md") {
			return SkillRepositoryContents{}, notFound("skill content not found")
		}
		readCtx := skillContentContext(ctx, request, contentservice.AccessReadPayload)
		body, meta, err := store.Content.OpenFile(readCtx, request.WorkspaceID, request.ContentID, request.VersionID, skillPath, nil)
		if err != nil {
			return SkillRepositoryContents{}, fmt.Errorf("read native skill content %q: %w", skillPath, err)
		}
		data, readErr := io.ReadAll(io.LimitReader(body, SkillRepositoryMaxSkillMDBytes+1))
		closeErr := body.Close()
		if readErr != nil || closeErr != nil {
			return SkillRepositoryContents{}, errors.Join(readErr, closeErr)
		}
		if meta.SizeBytes > SkillRepositoryMaxSkillMDBytes || len(data) > SkillRepositoryMaxSkillMDBytes {
			return SkillRepositoryContents{}, notFound("skill content not found")
		}
		frontmatter, err := parseSkillFrontmatter(data)
		if err != nil || strings.TrimSpace(frontmatter["name"]) == "" || strings.TrimSpace(frontmatter["description"]) == "" {
			return SkillRepositoryContents{}, notFound("skill content not found")
		}
		items = append(items, SkillRepositoryContent{Name: strings.TrimSpace(frontmatter["name"]), Path: skillPath, Content: string(data)})
	}
	return SkillRepositoryContents{VersionID: request.VersionID, Items: items}, nil
}

func (store NativeSkillContentStore) Import(ctx context.Context, request NativeSkillContentRequest, input SkillContentImportMutation) (NativeSkillVersionResult, error) {
	ctx, cancel := boundedSkillContentContext(ctx)
	defer cancel()
	root, cleanup, err := store.materialize(ctx, request, "meshx-skill-native-import-*")
	if err != nil {
		return NativeSkillVersionResult{}, err
	}
	defer cleanup()
	workDir := filepath.Dir(root)
	sourceDir, sourceCleanup, _, warnings, err := importSourceArchive(ctx, SkillRepositoryImportRequest{
		SourceFilePath: input.SourceFilePath, SourceName: input.SourceName, SourceURL: input.SourceURL, ReplaceSkills: input.ReplaceSkills,
	}, workDir)
	defer sourceCleanup()
	if err != nil {
		return NativeSkillVersionResult{}, err
	}
	scan, err := ScanSkillRepository(sourceDir, SkillRepositoryScanLimit)
	if err != nil {
		return NativeSkillVersionResult{}, err
	}
	imported, err := importScannedSkills(sourceDir, filepath.Join(root, "skills"), scan, input.ReplaceSkills)
	if err != nil {
		return NativeSkillVersionResult{}, err
	}
	merged, err := ScanSkillRepository(root, SkillRepositoryScanLimit)
	if err != nil {
		return NativeSkillVersionResult{}, err
	}
	if merged.Truncated {
		return NativeSkillVersionResult{}, codedInvalidArgument(SkillImportErrorScanLimitExceeded, "skill import merged repository scan limit exceeded")
	}
	valid := validDetectedSkills(merged.DetectedSkills)
	if err := updateReadmeSkillIndex(root, valid); err != nil {
		return NativeSkillVersionResult{}, err
	}
	content, err := store.exactBindingContent(ctx, request)
	if err != nil {
		return NativeSkillVersionResult{}, err
	}
	result, err := store.publishDirectory(ctx, request, content, root, "import")
	if err != nil {
		return NativeSkillVersionResult{}, err
	}
	result.ImportedSkills = imported
	result.Warnings = warnings
	return result, nil
}

func (store NativeSkillContentStore) Restore(ctx context.Context, request NativeSkillContentRequest, targetVersionID string) (NativeSkillVersionResult, error) {
	ctx, cancel := boundedSkillContentContext(ctx)
	defer cancel()
	target := request
	target.VersionID = strings.TrimSpace(targetVersionID)
	root, cleanup, err := store.materialize(ctx, target, "meshx-skill-native-restore-*")
	if err != nil {
		return NativeSkillVersionResult{}, err
	}
	defer cleanup()
	content, err := store.exactBindingContent(ctx, request)
	if err != nil {
		return NativeSkillVersionResult{}, err
	}
	return store.publishDirectory(ctx, request, content, root, "restore:"+target.VersionID)
}

func (store NativeSkillContentStore) CreateBackup(ctx context.Context, request NativeSkillContentRequest, label string) (contentservice.Pin, error) {
	if err := validateNativeSkillContentRequest(request); err != nil {
		return contentservice.Pin{}, err
	}
	ctx, cancel := boundedSkillContentContext(ctx)
	defer cancel()
	return store.Content.CreatePin(skillContentContext(ctx, request, contentservice.AccessManagePin), contentservice.CreatePinRequest{
		WorkspaceID: request.WorkspaceID, ContentID: request.ContentID, VersionID: request.VersionID,
		Label: strings.TrimSpace(label), Actor: request.Actor,
	})
}

func (store NativeSkillContentStore) ListBackups(ctx context.Context, request NativeSkillContentRequest, limit int) ([]contentservice.Pin, error) {
	if err := validateNativeSkillContentRequest(request); err != nil {
		return nil, err
	}
	ctx, cancel := boundedSkillContentContext(ctx)
	defer cancel()
	return store.Content.ListPins(skillContentContext(ctx, request, contentservice.AccessManagePin), request.WorkspaceID, request.ContentID, limit)
}

func (store NativeSkillContentStore) findOrCreateContent(ctx context.Context, workspaceID, skillID, actor string) (contentservice.Content, error) {
	request := NativeSkillContentRequest{WorkspaceID: workspaceID, SkillID: skillID, Actor: actor}
	readCtx := skillContentContext(ctx, request, contentservice.AccessReadMetadata)
	content, found, err := store.Content.FindContentByBinding(readCtx, workspaceID, skillContentDomain, skillID)
	if err != nil {
		return contentservice.Content{}, err
	}
	if found {
		return content, nil
	}
	createCtx := skillContentContext(ctx, request, contentservice.AccessCreateLineage)
	content, err = store.Content.CreateContent(createCtx, contentservice.CreateContentRequest{
		WorkspaceID: workspaceID, Domain: skillContentDomain, BindingKey: skillID,
		Model: contentservice.TreeModel, Actor: actor,
	})
	if err == nil {
		return content, nil
	}
	if !contentservice.IsBindingConflict(err) {
		return contentservice.Content{}, err
	}
	content, found, findErr := store.Content.FindContentByBinding(readCtx, workspaceID, skillContentDomain, skillID)
	if findErr != nil || !found {
		return contentservice.Content{}, errors.Join(err, findErr)
	}
	return content, nil
}

func (store NativeSkillContentStore) exactBindingContent(ctx context.Context, request NativeSkillContentRequest) (contentservice.Content, error) {
	if err := validateNativeSkillContentRequest(request); err != nil {
		return contentservice.Content{}, err
	}
	lookup := request
	lookup.ContentID = ""
	content, found, err := store.Content.FindContentByBinding(skillContentContext(ctx, lookup, contentservice.AccessReadMetadata), request.WorkspaceID, skillContentDomain, request.SkillID)
	if err != nil {
		return contentservice.Content{}, err
	}
	if !found || content.ID != strings.TrimSpace(request.ContentID) {
		return contentservice.Content{}, ErrSkillRepositorySnapshotChanged
	}
	return content, nil
}

func (store NativeSkillContentStore) publishDirectory(ctx context.Context, request NativeSkillContentRequest, content contentservice.Content, root, operation string) (NativeSkillVersionResult, error) {
	preview, err := ScanSkillRepository(root, SkillRepositoryScanLimit)
	if err != nil {
		return NativeSkillVersionResult{}, err
	}
	if preview.Truncated {
		return NativeSkillVersionResult{}, codedInvalidArgument(SkillImportErrorScanLimitExceeded, "skill repository scan limit exceeded")
	}
	files, treeDigest, err := nativeSkillTreeFiles(ctx, root)
	if err != nil {
		return NativeSkillVersionResult{}, err
	}
	request.ContentID = content.ID
	writeCtx := skillContentContext(ctx, request, contentservice.AccessPublish)
	operationKey := "skill.content:" + skillContentOperationDigest(request.SkillID, request.VersionID, operation, treeDigest)
	receipt, found, err := store.Content.FindVersionOperation(writeCtx, request.WorkspaceID, content.ID, operationKey)
	if err != nil {
		return NativeSkillVersionResult{}, fmt.Errorf("find native skill content operation: %w", err)
	}
	if found {
		if receipt.OperationNamespace != "skill.content" || receipt.OperationVersion != 1 || receipt.OperationKey != operationKey {
			return NativeSkillVersionResult{}, fmt.Errorf("native skill content operation receipt does not match request")
		}
		preview.VersionID = receipt.VersionID
		return NativeSkillVersionResult{
			ContentID: content.ID, VersionID: receipt.VersionID, RootDigest: receipt.RootDigest,
			FileCount: receipt.FileCount, SizeBytes: receipt.SizeBytes,
			SkillCount: validSkillRepositoryCount(preview.DetectedSkills), Preview: preview,
		}, nil
	}
	if content.HeadVersionID != strings.TrimSpace(request.VersionID) {
		return NativeSkillVersionResult{}, ErrSkillRepositorySnapshotChanged
	}
	committed, err := store.Content.CommitVersion(writeCtx, contentservice.CommitVersionRequest{
		WorkspaceID: request.WorkspaceID, ContentID: content.ID,
		ExpectedHeadVersionID: content.HeadVersionID, ExpectedGeneration: content.Generation,
		Actor: request.Actor, Model: contentservice.TreeModel, Checkpoint: true, Files: files,
		OperationKey:       operationKey,
		OperationNamespace: "skill.content", OperationVersion: 1,
		TreeLimits: contentpayload.Limits{MaxFiles: skillContentMaxFiles, MaxFileBytes: skillContentMaxFileBytes, MaxTotalBytes: skillContentMaxTotalBytes, MaxManifestBytes: skillContentMaxManifestBytes},
	})
	if err != nil {
		return NativeSkillVersionResult{}, fmt.Errorf("publish native skill content: %w", err)
	}
	preview.VersionID = committed.Version.ID
	return NativeSkillVersionResult{
		ContentID: content.ID, VersionID: committed.Version.ID, RootDigest: committed.Version.RootDigest,
		FileCount: committed.Version.FileCount, SizeBytes: committed.Version.SizeBytes,
		SkillCount: validSkillRepositoryCount(preview.DetectedSkills), Preview: preview,
	}, nil
}

func (store NativeSkillContentStore) materialize(ctx context.Context, request NativeSkillContentRequest, pattern string) (string, func(), error) {
	if store.Content == nil {
		return "", func() {}, invalidArgument("native skill content is not configured")
	}
	if err := validateNativeSkillContentRequest(request); err != nil {
		return "", func() {}, err
	}
	workDir, err := os.MkdirTemp("", pattern)
	if err != nil {
		return "", func() {}, err
	}
	root := filepath.Join(workDir, "tree")
	readCtx := skillContentContext(ctx, request, contentservice.AccessReadPayload)
	err = store.Content.MaterializeTree(readCtx, request.WorkspaceID, request.ContentID, request.VersionID, root, contentpayload.MaterializeLimits{
		MaxFiles: skillContentMaxFiles, MaxFileBytes: skillContentMaxFileBytes, MaxTotalBytes: skillContentMaxTotalBytes,
	})
	if err != nil {
		os.RemoveAll(workDir)
		return "", func() {}, fmt.Errorf("materialize native skill content: %w", err)
	}
	return root, func() { _ = os.RemoveAll(workDir) }, nil
}

func skillContentContext(ctx context.Context, request NativeSkillContentRequest, action contentservice.AccessAction) context.Context {
	return contentservice.WithAccessPrincipal(ctx, contentservice.AccessPrincipal{
		Subject: strings.TrimSpace(request.Actor), WorkspaceID: strings.TrimSpace(request.WorkspaceID), Domain: skillContentDomain,
		BindingKey: strings.TrimSpace(request.SkillID), ContentID: strings.TrimSpace(request.ContentID), Actions: []contentservice.AccessAction{action},
	})
}

func boundedSkillContentContext(ctx context.Context) (context.Context, context.CancelFunc) {
	if deadline, ok := ctx.Deadline(); ok && time.Until(deadline) <= skillContentOperationTimeout {
		return context.WithCancel(ctx)
	}
	return context.WithTimeout(ctx, skillContentOperationTimeout)
}

func validateNativeSkillContentRequest(request NativeSkillContentRequest) error {
	if strings.TrimSpace(request.WorkspaceID) == "" || strings.TrimSpace(request.SkillID) == "" || strings.TrimSpace(request.ContentID) == "" || strings.TrimSpace(request.VersionID) == "" || strings.TrimSpace(request.Actor) == "" {
		return invalidArgument("native skill content reference is incomplete")
	}
	return nil
}

func writeNativeSkillFile(root, relative string, data []byte) error {
	clean := filepath.Clean(filepath.FromSlash(relative))
	if clean == "." || filepath.IsAbs(clean) || strings.HasPrefix(clean, ".."+string(filepath.Separator)) {
		return invalidArgument("native skill path is invalid")
	}
	target := filepath.Join(root, clean)
	if err := os.MkdirAll(filepath.Dir(target), 0o755); err != nil {
		return err
	}
	return os.WriteFile(target, data, 0o644)
}

func nativeSkillTreeFiles(ctx context.Context, root string) ([]contentpayload.File, string, error) {
	type treeFile struct {
		path string
		data []byte
	}
	rows := make([]treeFile, 0, 64)
	var total int64
	err := filepath.WalkDir(root, func(path string, entry fs.DirEntry, walkErr error) error {
		if walkErr != nil {
			return walkErr
		}
		if err := ctx.Err(); err != nil {
			return err
		}
		rel, err := filepath.Rel(root, path)
		if err != nil {
			return err
		}
		rel = filepath.ToSlash(rel)
		if rel == "." {
			return nil
		}
		if rel == ".git" && entry.IsDir() {
			return filepath.SkipDir
		}
		if entry.Type()&os.ModeSymlink != 0 {
			return codedInvalidArgument(SkillRepositoryIssueSymlink, "native skill content contains symlink")
		}
		if entry.IsDir() {
			return nil
		}
		info, err := entry.Info()
		if err != nil {
			return err
		}
		if !info.Mode().IsRegular() || info.Size() > skillContentMaxFileBytes || len(rows) >= skillContentMaxFiles || total > skillContentMaxTotalBytes-info.Size() {
			return codedInvalidArgument(SkillImportErrorArchiveLimitExceeded, "native skill content exceeds file or byte bounds")
		}
		data, err := os.ReadFile(path)
		if err != nil {
			return err
		}
		if int64(len(data)) > skillContentMaxFileBytes || total > skillContentMaxTotalBytes-int64(len(data)) {
			return codedInvalidArgument(SkillImportErrorArchiveLimitExceeded, "native skill content exceeds file or byte bounds")
		}
		total += int64(len(data))
		rows = append(rows, treeFile{path: rel, data: data})
		return nil
	})
	if err != nil {
		return nil, "", err
	}
	sort.Slice(rows, func(i, j int) bool { return rows[i].path < rows[j].path })
	hash := sha256.New()
	files := make([]contentpayload.File, 0, len(rows))
	for _, row := range rows {
		hash.Write([]byte(row.path))
		hash.Write([]byte{0})
		hash.Write(row.data)
		hash.Write([]byte{0})
		files = append(files, contentpayload.File{Path: row.path, Size: int64(len(row.data)), Content: bytes.NewReader(row.data)})
	}
	return files, hex.EncodeToString(hash.Sum(nil)), nil
}

func skillContentOperationDigest(parts ...string) string {
	hash := sha256.New()
	for _, part := range parts {
		hash.Write([]byte(strings.TrimSpace(part)))
		hash.Write([]byte{0})
	}
	return hex.EncodeToString(hash.Sum(nil))
}

var _ NativeSkillContent = NativeSkillContentStore{}
