package automation

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

	"github.com/tidbcloud/AgentTeam/internal/agsbridge/wiki"
	"github.com/tidbcloud/AgentTeam/internal/service/automation/runtime"
	"github.com/tidbcloud/AgentTeam/internal/service/resource/repos"
)

const (
	defaultAutomationWikiPublishMaxPages     = 5000
	defaultAutomationWikiPublishMaxEntries   = 10000
	defaultAutomationWikiPublishMaxPageBytes = int64(2 * 1024 * 1024)

	automationWikiPublishMaxSlugLength    = 255
	automationWikiPublishMaxSlugDepth     = 6
	automationWikiPublishMaxSegmentLength = 64
	automationWikiPublishSlugHashLength   = 8
)

var errAutomationWikiPublishLimitExceeded = errors.New("automation wiki publish limit exceeded")

// AGSAutomationWikiPublisher publishes staged Markdown files into AGS wiki pages.
type AGSAutomationWikiPublisher struct {
	Store        wiki.AGSRepositoryStore
	MaxPages     int
	MaxEntries   int
	MaxPageBytes int64
}

func (p AGSAutomationWikiPublisher) PublishWikiPages(ctx context.Context, req runtime.WikiPublishRequest) (runtime.WikiPublishResult, error) {
	if p.Store == nil {
		return runtime.WikiPublishResult{}, invalidArgument("automation wiki publisher is not configured")
	}
	owner, name, err := automationPublishRepository(req.Repository)
	if err != nil {
		return runtime.WikiPublishResult{}, err
	}
	sourceRoot := strings.TrimSpace(req.SourcePath)
	if sourceRoot == "" || !filepath.IsAbs(sourceRoot) {
		return runtime.WikiPublishResult{}, invalidArgument("automation wiki publish source path must be absolute")
	}
	limits := p.limits(req.MaxPages)
	pages, bytesRead, err := readAutomationWikiPublishPages(sourceRoot, limits)
	if err != nil {
		return runtime.WikiPublishResult{}, err
	}
	labels := normalizeLabels([]string{req.Label})
	repository, err := p.Store.EnsureRepository(ctx, repos.AGSRepositoryEnsureRequest{
		WorkspaceID:   strings.TrimSpace(req.WorkspaceID),
		ResourceType:  "automation_run",
		ResourceID:    strings.TrimSpace(req.RunID),
		RepoRole:      "wiki",
		Owner:         owner,
		Name:          name,
		Description:   firstNonEmptyString(req.Description, "Automation-generated wiki pages"),
		Kind:          firstNonEmptyString(req.RepositoryKind, "automation_preset_wiki"),
		Visibility:    firstNonEmptyString(req.Visibility, "private"),
		UserVisible:   true,
		AutoInit:      true,
		DefaultBranch: firstNonEmptyString(req.DefaultBranch, "main"),
		Features:      repos.AGSRepositoryFeatures{Wiki: true},
	})
	if err != nil {
		return runtime.WikiPublishResult{}, err
	}
	if len(labels) > 0 {
		keepSlugs := make([]string, 0, len(pages))
		for _, page := range pages {
			keepSlugs = append(keepSlugs, page.Slug)
		}
		if err := p.Store.PrunePages(ctx, wiki.KBWikiPrune{
			Repository: repository,
			Labels:     labels,
			KeepSlugs:  keepSlugs,
			Message:    firstNonEmptyString(req.Message, "Prune stale automation wiki pages"),
		}); err != nil {
			return runtime.WikiPublishResult{}, err
		}
	}
	for _, page := range pages {
		page.Labels = labels
		if err := p.Store.PutPage(ctx, wiki.KBWikiPageWrite{Repository: repository, Page: page, Message: firstNonEmptyString(req.Message, "Publish automation wiki page "+page.Slug)}); err != nil {
			return runtime.WikiPublishResult{}, err
		}
	}
	reconciled, err := p.Store.Reconcile(ctx, repository)
	if err != nil {
		return runtime.WikiPublishResult{}, err
	}
	return runtime.WikiPublishResult{
		Repository:       repository.FullName,
		Pages:            len(pages),
		Bytes:            bytesRead,
		IndexedCommitSHA: reconciled.IndexedCommitSHA,
		Reconciled:       reconciled.Reconciled,
	}, nil
}

type automationWikiPublishLimits struct {
	MaxPages     int
	MaxEntries   int
	MaxPageBytes int64
	MaxBytes     int64
}

func (p AGSAutomationWikiPublisher) limits(requestMaxPages int) automationWikiPublishLimits {
	maxPages := p.MaxPages
	if maxPages <= 0 {
		maxPages = defaultAutomationWikiPublishMaxPages
	}
	if requestMaxPages > 0 && requestMaxPages < maxPages {
		maxPages = requestMaxPages
	}
	maxEntries := p.MaxEntries
	if maxEntries <= 0 {
		maxEntries = defaultAutomationWikiPublishMaxEntries
	}
	maxPageBytes := p.MaxPageBytes
	if maxPageBytes <= 0 {
		maxPageBytes = defaultAutomationWikiPublishMaxPageBytes
	}
	return automationWikiPublishLimits{MaxPages: maxPages, MaxEntries: maxEntries, MaxPageBytes: maxPageBytes}
}

func readAutomationWikiPublishPages(root string, limits automationWikiPublishLimits) ([]wiki.GeneratedWikiPage, int64, error) {
	var paths []string
	entries := 0
	err := filepath.WalkDir(root, func(path string, entry os.DirEntry, walkErr error) error {
		if walkErr != nil {
			return walkErr
		}
		entries++
		if entries > limits.MaxEntries {
			return errAutomationWikiPublishLimitExceeded
		}
		if entry.IsDir() {
			if entry.Name() != "." && strings.HasPrefix(entry.Name(), ".") {
				return filepath.SkipDir
			}
			return nil
		}
		if entry.Type()&os.ModeSymlink != 0 {
			return nil
		}
		if strings.EqualFold(filepath.Ext(entry.Name()), ".md") {
			paths = append(paths, path)
			if len(paths) > limits.MaxPages {
				return errAutomationWikiPublishLimitExceeded
			}
		}
		return nil
	})
	if errors.Is(err, errAutomationWikiPublishLimitExceeded) {
		return nil, 0, invalidArgument(fmt.Sprintf("automation wiki publish source exceeds %d markdown pages or %d filesystem entries", limits.MaxPages, limits.MaxEntries))
	}
	if err != nil {
		return nil, 0, fmt.Errorf("read automation wiki publish source: %w", err)
	}
	sort.Slice(paths, func(i, j int) bool {
		return agentteamCodeWikiPageSortKey(root, paths[i]) < agentteamCodeWikiPageSortKey(root, paths[j])
	})
	pages := make([]wiki.GeneratedWikiPage, 0, len(paths))
	seen := map[string]struct{}{}
	var bytesRead int64
	for _, path := range paths {
		info, err := os.Stat(path)
		if err != nil {
			return nil, 0, fmt.Errorf("stat automation wiki page: %w", err)
		}
		if info.Size() > limits.MaxPageBytes {
			return nil, 0, invalidArgument(fmt.Sprintf("automation wiki page %q exceeds %d bytes", path, limits.MaxPageBytes))
		}
		body, err := os.ReadFile(path)
		if err != nil {
			return nil, 0, fmt.Errorf("read automation wiki page: %w", err)
		}
		rel, err := filepath.Rel(root, path)
		if err != nil {
			return nil, 0, fmt.Errorf("resolve automation wiki page path: %w", err)
		}
		slug := automationWikiPublishSlug(rel)
		if slug == "" {
			continue
		}
		slug = automationWikiPublishResolveSlugConflict(slug, rel, seen)
		if slug == "" {
			return nil, 0, invalidArgument("automation wiki page slugs must be unique and prefix-free")
		}
		seen[slug] = struct{}{}
		bytesRead += int64(len(body))
		if limits.MaxBytes > 0 && bytesRead > limits.MaxBytes {
			return nil, 0, invalidArgument(fmt.Sprintf("automation wiki publish source exceeds %d bytes", limits.MaxBytes))
		}
		pages = append(pages, wiki.GeneratedWikiPage{
			Slug:  slug,
			Title: agentteamCodeWikiTitle(string(body), slug),
			Body:  string(body),
		})
	}
	if len(pages) == 0 {
		return nil, 0, invalidArgument("automation wiki publish source contains no markdown pages")
	}
	return pages, bytesRead, nil
}

func automationWikiPublishSlug(path string) string {
	path = filepath.ToSlash(filepath.Clean(strings.TrimSpace(path)))
	path = strings.Trim(path, "/")
	if path == "" || path == "." {
		return ""
	}
	if strings.EqualFold(path, "README.md") {
		return "overview"
	}

	parts := strings.Split(path, "/")
	if len(parts) == 0 {
		return ""
	}
	leaf := parts[len(parts)-1]
	if strings.EqualFold(leaf, "README.md") {
		parts[len(parts)-1] = "overview"
	} else {
		parts[len(parts)-1] = strings.TrimSuffix(leaf, filepath.Ext(leaf))
	}

	segments := make([]string, 0, len(parts))
	for _, part := range parts {
		segment := automationWikiPublishSlugSegment(part)
		if segment == "" {
			continue
		}
		segments = append(segments, segment)
	}
	if len(segments) == 0 {
		return ""
	}
	if len(segments) > automationWikiPublishMaxSlugDepth {
		segments = automationWikiPublishFlattenDeepSlugSegments(segments)
	}
	segments = automationWikiPublishConstrainSlugSegments(segments, path)
	return strings.Join(segments, "/")
}

func automationWikiPublishSlugSegment(value string) string {
	value = strings.ToLower(strings.TrimSpace(value))
	if value == "_sidebar" {
		return value
	}
	var out strings.Builder
	lastDash := false
	for _, r := range value {
		switch {
		case r >= 'a' && r <= 'z':
			out.WriteRune(r)
			lastDash = false
		case r >= '0' && r <= '9':
			out.WriteRune(r)
			lastDash = false
		case r == '-' || r == '_' || r == '.' || r == ' ':
			if out.Len() > 0 && !lastDash {
				out.WriteByte('-')
				lastDash = true
			}
		default:
			if out.Len() > 0 && !lastDash {
				out.WriteByte('-')
				lastDash = true
			}
		}
	}
	segment := strings.Trim(out.String(), "-")
	if segment == "." || segment == ".." {
		return ""
	}
	return segment
}

func automationWikiPublishFlattenDeepSlugSegments(segments []string) []string {
	out := append([]string{}, segments[:automationWikiPublishMaxSlugDepth-1]...)
	tail := automationWikiPublishSlugSegment(strings.Join(segments[automationWikiPublishMaxSlugDepth-1:], "-"))
	if tail != "" {
		out = append(out, tail)
	}
	return out
}

func automationWikiPublishConstrainSlugSegments(segments []string, hashInput string) []string {
	out := append([]string{}, segments...)
	for i, segment := range out {
		out[i] = automationWikiPublishShortenSlugSegment(segment, hashInput+"\x00segment\x00"+segment, automationWikiPublishMaxSegmentLength)
	}
	for automationWikiPublishJoinedSlugLength(out) > automationWikiPublishMaxSlugLength {
		index := automationWikiPublishLongestShrinkableSlugSegment(out)
		if index < 0 {
			break
		}
		overBy := automationWikiPublishJoinedSlugLength(out) - automationWikiPublishMaxSlugLength
		maxLen := len(out[index]) - overBy
		if maxLen < automationWikiPublishSlugHashLength {
			maxLen = automationWikiPublishSlugHashLength
		}
		if maxLen >= len(out[index]) {
			maxLen = len(out[index]) - 1
		}
		out[index] = automationWikiPublishShortenSlugSegment(out[index], hashInput+"\x00slug\x00"+strings.Join(out, "/"), maxLen)
	}
	return out
}

func automationWikiPublishJoinedSlugLength(segments []string) int {
	if len(segments) == 0 {
		return 0
	}
	length := len(segments) - 1
	for _, segment := range segments {
		length += len(segment)
	}
	return length
}

func automationWikiPublishLongestShrinkableSlugSegment(segments []string) int {
	index := -1
	longest := automationWikiPublishSlugHashLength
	for i, segment := range segments {
		if len(segment) > longest {
			index = i
			longest = len(segment)
		}
	}
	return index
}

func automationWikiPublishResolveSlugConflict(slug string, relPath string, seen map[string]struct{}) string {
	for attempt := 0; attempt < 64; attempt++ {
		if _, ok := seen[slug]; ok {
			slug = automationWikiPublishDisambiguateSlug(slug, relPath, seen)
			if slug == "" {
				return ""
			}
			continue
		}
		conflict := automationWikiPublishSlugPrefixConflict(slug, seen)
		if conflict == "" {
			return slug
		}
		slug = automationWikiPublishDisambiguatePrefixSlug(slug, relPath, conflict)
		if slug == "" {
			return ""
		}
	}
	return ""
}

func automationWikiPublishSlugPrefixConflict(slug string, seen map[string]struct{}) string {
	for existing := range seen {
		if strings.HasPrefix(slug, existing+"/") || strings.HasPrefix(existing, slug+"/") {
			return existing
		}
	}
	return ""
}

func automationWikiPublishDisambiguatePrefixSlug(slug string, relPath string, conflict string) string {
	if strings.HasPrefix(conflict, slug+"/") {
		return automationWikiPublishSlugAsOverview(slug, relPath)
	}
	return automationWikiPublishFlattenSlug(slug, relPath)
}

func automationWikiPublishSlugAsOverview(slug string, relPath string) string {
	segments := strings.Split(slug, "/")
	if len(segments) < automationWikiPublishMaxSlugDepth {
		segments = append(segments, "overview")
		return strings.Join(automationWikiPublishConstrainSlugSegments(segments, relPath+"\x00overview"), "/")
	}
	last := len(segments) - 1
	segments[last] = automationWikiPublishShortenSlugSegment(segments[last]+"-overview", relPath+"\x00overview", automationWikiPublishMaxSegmentLength)
	return strings.Join(automationWikiPublishConstrainSlugSegments(segments, relPath+"\x00overview"), "/")
}

func automationWikiPublishFlattenSlug(slug string, relPath string) string {
	segment := automationWikiPublishSlugSegment(strings.ReplaceAll(slug, "/", "-"))
	if segment == "" {
		segment = "page"
	}
	return automationWikiPublishShortenSlugSegment(segment, relPath+"\x00flat", automationWikiPublishMaxSegmentLength)
}

func automationWikiPublishShortenSlugSegment(segment string, hashInput string, maxLen int) string {
	if len(segment) <= maxLen {
		return segment
	}
	if maxLen <= automationWikiPublishSlugHashLength {
		return automationWikiPublishSlugHash(hashInput, maxLen)
	}
	suffix := "-" + automationWikiPublishSlugHash(hashInput, min(automationWikiPublishSlugHashLength, maxLen-1))
	prefixLen := maxLen - len(suffix)
	prefix := segment
	if prefix == "_sidebar" {
		prefix = "sidebar"
	}
	if len(prefix) > prefixLen {
		prefix = prefix[:prefixLen]
	}
	prefix = strings.Trim(prefix, "-")
	if prefix == "" {
		return strings.Trim(suffix, "-")
	}
	return prefix + suffix
}

func automationWikiPublishDisambiguateSlug(slug string, relPath string, seen map[string]struct{}) string {
	for hashLen := automationWikiPublishSlugHashLength; hashLen <= sha256.Size*2; hashLen += 4 {
		candidate := automationWikiPublishSlugWithHash(slug, relPath, hashLen)
		if _, ok := seen[candidate]; !ok {
			return candidate
		}
	}
	return ""
}

func automationWikiPublishSlugWithHash(slug string, relPath string, hashLen int) string {
	segments := strings.Split(slug, "/")
	index := len(segments) - 1
	suffix := "-" + automationWikiPublishSlugHash(relPath, hashLen)
	maxLeafLen := automationWikiPublishMaxSegmentLength - len(suffix)
	if maxLeafLen < 1 {
		maxLeafLen = 1
	}
	leaf := segments[index]
	if leaf == "_sidebar" {
		leaf = "sidebar"
	}
	if len(leaf) > maxLeafLen {
		leaf = leaf[:maxLeafLen]
	}
	leaf = strings.Trim(leaf, "-")
	if leaf == "" {
		leaf = automationWikiPublishSlugHash(relPath+"\x00leaf", min(hashLen, maxLeafLen))
	}
	segments[index] = leaf + suffix
	segments = automationWikiPublishConstrainSlugSegments(segments, relPath+"\x00collision")
	return strings.Join(segments, "/")
}

func automationWikiPublishSlugHash(value string, size int) string {
	if size <= 0 {
		return ""
	}
	sum := sha256.Sum256([]byte(value))
	digest := hex.EncodeToString(sum[:])
	if size > len(digest) {
		size = len(digest)
	}
	return digest[:size]
}

func normalizeLabels(labels []string) []string {
	seen := map[string]struct{}{}
	out := make([]string, 0, len(labels))
	for _, label := range labels {
		label = strings.ToLower(strings.TrimSpace(label))
		if label == "" {
			continue
		}
		if _, ok := seen[label]; ok {
			continue
		}
		seen[label] = struct{}{}
		out = append(out, label)
	}
	return out
}

func agentteamCodeWikiPageSortKey(root string, path string) string {
	rel, err := filepath.Rel(root, path)
	if err != nil {
		return path
	}
	rel = filepath.ToSlash(rel)
	if strings.EqualFold(rel, "README.md") {
		return "\x00"
	}
	return rel
}

func agentteamCodeWikiTitle(body string, slug string) string {
	for _, line := range strings.Split(body, "\n") {
		line = strings.TrimSpace(line)
		if strings.HasPrefix(line, "# ") {
			title := strings.TrimSpace(strings.TrimPrefix(line, "# "))
			if title != "" {
				return title
			}
		}
	}
	base := filepath.Base(strings.Trim(slug, "/"))
	base = strings.ReplaceAll(base, "-", " ")
	base = strings.ReplaceAll(base, "_", " ")
	return strings.Title(base)
}
