package skills

import (
	"errors"
	"fmt"
	"hash/fnv"
	"io/fs"
	"os"
	"path/filepath"
	"regexp"
	"sort"
	"strings"
	"unicode"

	"github.com/tidbcloud/AgentTeam/internal/service/resource/repos"
	"gopkg.in/yaml.v3"
)

const (
	SkillRepositoryScanLimit            = 200
	SkillRepositoryMaxSkillMDBytes      = 256 * 1024
	SkillRepositoryContentsPathLimit    = SkillRepositoryScanLimit
	SkillRepositoryContentsPathMaxBytes = 1024

	SkillRepositorySkillStatusValid    = "valid"
	SkillRepositorySkillStatusRejected = "rejected"

	SkillRepositoryLayoutRoot       = "root"
	SkillRepositoryLayoutCollection = "collection"
	SkillRepositoryLayoutTemplate   = "template"

	SkillRepositoryIssueOK                 = "OK"
	SkillRepositoryIssueOversize           = "OVERSIZE"
	SkillRepositoryIssueMissingName        = "MISSING_FRONTMATTER_NAME"
	SkillRepositoryIssueMissingDescription = "MISSING_FRONTMATTER_DESCRIPTION"
	SkillRepositoryIssueFrontmatterInvalid = "FRONTMATTER_INVALID"
	SkillRepositoryIssueSymlink            = "SYMLINK"
	SkillRepositoryIssuePathTraversal      = "PATH_TRAVERSAL"
	SkillRepositoryIssueDuplicateName      = "DUPLICATE_NAME"

	SkillImportErrorInvalidSource         = "INVALID_IMPORT_SOURCE"
	SkillImportErrorUnsupportedScheme     = "UNSUPPORTED_SCHEME"
	SkillImportErrorUnsafeRemoteHost      = "UNSAFE_REMOTE_HOST"
	SkillImportErrorDownloadFailed        = "DOWNLOAD_FAILED"
	SkillImportErrorUnsupportedArchive    = "UNSUPPORTED_ARCHIVE"
	SkillImportErrorArchiveUnsafe         = "UNSAFE_ARCHIVE"
	SkillImportErrorArchiveLimitExceeded  = "ARCHIVE_LIMIT_EXCEEDED"
	SkillImportErrorNoSkillsFound         = "NO_SKILLS_FOUND"
	SkillImportErrorScanLimitExceeded     = "SCAN_LIMIT_EXCEEDED"
	SkillImportErrorRepositoryUnavailable = "REPOSITORY_UNAVAILABLE"
	SkillImportErrorPermissionDenied      = "PERMISSION_DENIED"
	SkillImportErrorCopyFailed            = "COPY_FAILED"

	SkillRepositoryErrorSnapshotChanged = "SKILL_REPOSITORY_SNAPSHOT_CHANGED"
)

var (
	skillRepositoryDirectoryNamePattern = regexp.MustCompile(`^[a-z0-9][a-z0-9-]{0,63}$`)
	skillCommitSHAPattern               = regexp.MustCompile(`^[0-9a-f]{40}$`)
)

type SkillRepositoryDetectedSkill struct {
	Name        string
	Description string
	Path        string
	Layout      string
	SizeBytes   int64
	Status      string
	IssueCode   string
	Frontmatter []string
}

type SkillRepositoryScanData struct {
	CommitSHA            string
	VersionID            string
	DetectedSkills       []SkillRepositoryDetectedSkill
	SuggestedName        string
	SuggestedSlug        string
	SuggestedDescription string
	Truncated            bool
	Limit                int
	IssueCode            string
}

type SkillRepositoryContent struct {
	Name    string
	Path    string
	Content string
}

type SkillRepositoryContentsRequest struct {
	ExpectedCommitSHA string
	ExpectedVersionID string
	Paths             []string
}

type SkillRepositoryContents struct {
	CommitSHA string
	VersionID string
	Items     []SkillRepositoryContent
}

type SkillRepositoryInitializeRequest struct {
	Repository  repos.RepositoryIdentity
	Slug        string
	Name        string
	Description string
	Locale      string
	Message     string
	AuthorName  string
	AuthorEmail string
}

type SkillRepositoryImportRequest struct {
	TargetRepository repos.RepositoryIdentity
	SourceFilePath   string
	SourceName       string
	SourceURL        string
	ReplaceSkills    bool
	Message          string
	AuthorName       string
	AuthorEmail      string
}

const (
	SkillImportWarningArchiveEntrySkippedOversize       = "ARCHIVE_ENTRY_SKIPPED_OVERSIZE"
	SkillImportWarningArchiveEntrySkippedExtractedLimit = "ARCHIVE_ENTRY_SKIPPED_EXTRACTED_LIMIT"
)

type SkillImportWarning struct {
	Code       string
	Message    string
	Path       string
	SizeBytes  int64
	LimitBytes int64
}

type SkillRepositoryImportResult struct {
	CommitSHA      string
	VersionID      string
	SkillCount     int
	ImportedSkills []SkillRepositoryDetectedSkill
	Warnings       []SkillImportWarning
}

func ScanSkillRepository(root string, limit int) (SkillRepositoryScanData, error) {
	if limit <= 0 {
		limit = SkillRepositoryScanLimit
	}
	root = filepath.Clean(root)
	info, err := os.Lstat(root)
	if err != nil {
		return SkillRepositoryScanData{}, err
	}
	if info.Mode()&os.ModeSymlink != 0 {
		return SkillRepositoryScanData{Limit: limit, DetectedSkills: []SkillRepositoryDetectedSkill{{Path: ".", Status: SkillRepositorySkillStatusRejected, IssueCode: SkillRepositoryIssueSymlink}}}, nil
	}
	if !info.IsDir() {
		return SkillRepositoryScanData{}, fmt.Errorf("skill repository root is not a directory")
	}
	var rows []SkillRepositoryDetectedSkill
	seenDirectoryNames := map[string]int{}
	truncated := false
	addRow := func(row SkillRepositoryDetectedSkill) error {
		if len(rows) >= limit {
			truncated = true
			return fs.SkipAll
		}
		if strings.TrimSpace(row.Status) == "" {
			row.Status = SkillRepositorySkillStatusRejected
		}
		if strings.TrimSpace(row.IssueCode) == "" {
			row.IssueCode = SkillRepositoryIssueOK
		}
		if row.Status == SkillRepositorySkillStatusValid {
			key := skillRepositoryDirectoryNameForSkill(row.Name, row.Path)
			if first, ok := seenDirectoryNames[key]; ok {
				row.Status = SkillRepositorySkillStatusRejected
				row.IssueCode = SkillRepositoryIssueDuplicateName
				rows[first].Status = SkillRepositorySkillStatusRejected
				rows[first].IssueCode = SkillRepositoryIssueDuplicateName
			} else {
				seenDirectoryNames[key] = len(rows)
			}
		}
		rows = append(rows, row)
		return nil
	}
	err = filepath.WalkDir(root, func(path string, d fs.DirEntry, walkErr error) error {
		if walkErr != nil {
			return walkErr
		}
		rel, err := filepath.Rel(root, path)
		if err != nil {
			return err
		}
		rel = filepath.ToSlash(rel)
		if rel == "." {
			return nil
		}
		if rel == ".git" && d.IsDir() {
			return filepath.SkipDir
		}
		if d.Type()&os.ModeSymlink != 0 {
			row := SkillRepositoryDetectedSkill{Path: rel, Status: SkillRepositorySkillStatusRejected, IssueCode: SkillRepositoryIssueSymlink}
			if err := addRow(row); err != nil {
				return err
			}
			if d.IsDir() {
				return filepath.SkipDir
			}
			return nil
		}
		if d.IsDir() {
			return nil
		}
		if !strings.EqualFold(filepath.Base(path), "SKILL.md") {
			return nil
		}
		row, err := readDetectedSkill(path, rel)
		if err != nil {
			return err
		}
		return addRow(row)
	})
	if errors.Is(err, fs.SkipAll) {
		err = nil
	}
	if err != nil {
		return SkillRepositoryScanData{}, err
	}
	sort.SliceStable(rows, func(i, j int) bool { return rows[i].Path < rows[j].Path })
	preview := SkillRepositoryScanData{DetectedSkills: rows, Limit: limit, Truncated: truncated}
	if truncated {
		preview.IssueCode = SkillImportErrorScanLimitExceeded
	}
	for _, row := range rows {
		if row.Status == SkillRepositorySkillStatusValid {
			preview.SuggestedName = row.Name
			preview.SuggestedSlug = skillRepositoryDirectoryNameForSkill(row.Name, row.Path)
			preview.SuggestedDescription = row.Description
			break
		}
	}
	return preview, nil
}

func skillLayoutFromPath(rel string) string {
	rel = filepath.ToSlash(filepath.Clean(rel))
	if strings.EqualFold(rel, "SKILL.md") {
		return SkillRepositoryLayoutRoot
	}
	parts := strings.Split(rel, "/")
	if len(parts) >= 3 && strings.EqualFold(parts[len(parts)-3], "skills") && strings.EqualFold(parts[len(parts)-1], "SKILL.md") {
		return SkillRepositoryLayoutTemplate
	}
	return SkillRepositoryLayoutCollection
}

func readDetectedSkill(path, rel string) (SkillRepositoryDetectedSkill, error) {
	info, err := os.Lstat(path)
	if err != nil {
		return SkillRepositoryDetectedSkill{}, err
	}
	row := SkillRepositoryDetectedSkill{Path: filepath.ToSlash(rel), Layout: skillLayoutFromPath(rel), SizeBytes: info.Size(), Status: SkillRepositorySkillStatusRejected}
	if info.Mode()&os.ModeSymlink != 0 {
		row.IssueCode = SkillRepositoryIssueSymlink
		return row, nil
	}
	if info.Size() > SkillRepositoryMaxSkillMDBytes {
		row.IssueCode = SkillRepositoryIssueOversize
		return row, nil
	}
	content, err := os.ReadFile(path)
	if err != nil {
		return SkillRepositoryDetectedSkill{}, err
	}
	frontmatter, err := parseSkillFrontmatter(content)
	if err != nil {
		row.IssueCode = SkillRepositoryIssueFrontmatterInvalid
		return row, nil
	}
	name := strings.TrimSpace(frontmatter["name"])
	description := strings.TrimSpace(frontmatter["description"])
	if name == "" {
		row.IssueCode = SkillRepositoryIssueMissingName
		return row, nil
	}
	row.Name = name
	row.Frontmatter = frontmatterKeys(frontmatter)
	if !skillDisplayNameSafe(name) {
		row.Description = description
		row.IssueCode = SkillRepositoryIssueFrontmatterInvalid
		return row, nil
	}
	if description == "" {
		row.IssueCode = SkillRepositoryIssueMissingDescription
		return row, nil
	}
	row.Description = description
	row.Status = SkillRepositorySkillStatusValid
	row.IssueCode = SkillRepositoryIssueOK
	return row, nil
}

func skillDisplayNameSafe(name string) bool {
	if strings.ContainsAny(name, `/\`) || strings.Contains(name, "..") {
		return false
	}
	for _, r := range name {
		if unicode.IsControl(r) {
			return false
		}
	}
	return true
}

func skillRepositoryDirectoryNameForSkill(name, rel string) string {
	if directoryName := skillRepositoryDirectoryNameFromValue(name); directoryName != "" {
		return directoryName
	}
	if directoryName := skillRepositoryDirectoryNameFromPath(rel); directoryName != "" {
		return directoryName
	}
	return skillRepositoryFallbackDirectoryName(name, rel)
}

func skillRepositoryDirectoryNameFromValue(value string) string {
	directoryName := skillSlug(value)
	if len(directoryName) > 64 {
		directoryName = strings.Trim(directoryName[:64], "-")
	}
	if !skillRepositoryDirectoryNamePattern.MatchString(directoryName) {
		return ""
	}
	return directoryName
}

func skillRepositoryDirectoryNameFromPath(rel string) string {
	clean := filepath.ToSlash(filepath.Clean(filepath.FromSlash(rel)))
	if clean == "." || clean == "/" || filepath.IsAbs(clean) || strings.HasPrefix(clean, "../") {
		return ""
	}
	dir := filepath.Dir(clean)
	if dir != "." && dir != "/" {
		if directoryName := skillRepositoryDirectoryNameFromValue(filepath.Base(dir)); directoryName != "" {
			return directoryName
		}
	}
	return ""
}

func skillRepositoryFallbackDirectoryName(name, rel string) string {
	hash := fnv.New64a()
	_, _ = hash.Write([]byte(strings.TrimSpace(name)))
	_, _ = hash.Write([]byte{0})
	_, _ = hash.Write([]byte(filepath.ToSlash(strings.TrimSpace(rel))))
	return fmt.Sprintf("skill-%016x", hash.Sum64())
}

func parseSkillFrontmatter(content []byte) (map[string]string, error) {
	text := string(content)
	if !strings.HasPrefix(text, "---\n") && !strings.HasPrefix(text, "---\r\n") {
		return map[string]string{}, nil
	}
	lines := strings.SplitAfter(text, "\n")
	var b strings.Builder
	found := false
	for i := 1; i < len(lines); i++ {
		trimmed := strings.TrimSpace(lines[i])
		if trimmed == "---" {
			found = true
			break
		}
		b.WriteString(lines[i])
	}
	if !found {
		return nil, fmt.Errorf("unterminated frontmatter")
	}
	var raw map[string]any
	if err := yaml.Unmarshal([]byte(b.String()), &raw); err != nil {
		return nil, err
	}
	out := make(map[string]string, len(raw))
	for k, v := range raw {
		if s, ok := v.(string); ok {
			out[strings.TrimSpace(k)] = strings.TrimSpace(s)
		}
	}
	return out, nil
}

func frontmatterKeys(values map[string]string) []string {
	keys := make([]string, 0, len(values))
	for key := range values {
		if strings.TrimSpace(values[key]) != "" {
			keys = append(keys, key)
		}
	}
	sort.Strings(keys)
	return keys
}
