package main

import (
	"encoding/json"
	"os"
	"path/filepath"
	"regexp"
	"strings"

	"gopkg.in/yaml.v3"
)

type routeIndex struct {
	targetLang      string
	redirects       map[string]string
	sourceRoutes    map[string]struct{}
	localizedRoutes map[string]struct{}
	localePrefixes  map[string]struct{}
}

type docsConfig struct {
	Redirects []docsRedirect `json:"redirects"`
}

type docsRedirect struct {
	Source      string `json:"source"`
	Destination string `json:"destination"`
}

var (
	localeDirRe             = regexp.MustCompile(`^[a-z]{2,3}(?:-[A-Za-z0-9]{2,8})?$`)
	fencedBacktickCodeBlock = regexp.MustCompile("(?ms)(^|\\n)[ \\t]*```[^\\n]*\\n.*?\\n[ \\t]*```[ \\t]*(?:\\n|$)")
	fencedTildeCodeBlock    = regexp.MustCompile("(?ms)(^|\\n)[ \\t]*~~~[^\\n]*\\n.*?\\n[ \\t]*~~~[ \\t]*(?:\\n|$)")
	markdownLinkTargetRe    = regexp.MustCompile(`!?\[[^\]]*\]\(([^)]+)\)`)
	hrefDoubleQuotedValueRe = regexp.MustCompile(`\bhref\s*=\s*"([^"]*)"`)
	hrefSingleQuotedValueRe = regexp.MustCompile(`\bhref\s*=\s*'([^']*)'`)
)

func loadRouteIndex(docsRoot, targetLang string) (*routeIndex, error) {
	index := &routeIndex{
		targetLang:      strings.TrimSpace(targetLang),
		redirects:       map[string]string{},
		sourceRoutes:    map[string]struct{}{},
		localizedRoutes: map[string]struct{}{},
		localePrefixes:  map[string]struct{}{},
	}

	if err := index.loadRedirects(filepath.Join(docsRoot, "docs.json")); err != nil {
		return nil, err
	}
	if err := index.loadRoutes(docsRoot); err != nil {
		return nil, err
	}

	return index, nil
}

func (ri *routeIndex) loadRedirects(configPath string) error {
	data, err := os.ReadFile(configPath)
	if err != nil {
		return err
	}
	var config docsConfig
	if err := json.Unmarshal(data, &config); err != nil {
		return err
	}
	for _, item := range config.Redirects {
		source := normalizeRoute(item.Source)
		destination := normalizeRoute(item.Destination)
		if source == "" || destination == "" {
			continue
		}
		ri.redirects[source] = destination
	}
	return nil
}

func (ri *routeIndex) loadRoutes(docsRoot string) error {
	localePrefixes, err := discoverLocalePrefixes(docsRoot)
	if err != nil {
		return err
	}
	if ri.targetLang != "" {
		localePrefixes[ri.targetLang] = struct{}{}
	}
	ri.localePrefixes = localePrefixes

	return filepath.WalkDir(docsRoot, func(path string, entry os.DirEntry, walkErr error) error {
		if walkErr != nil {
			return walkErr
		}
		if entry.IsDir() {
			return nil
		}
		if !isMarkdownFile(path) {
			return nil
		}

		relPath, err := filepath.Rel(docsRoot, path)
		if err != nil {
			return err
		}
		relPath = normalizeSlashes(relPath)
		firstSegment := firstPathSegment(relPath)

		content, err := os.ReadFile(path)
		if err != nil {
			return err
		}
		permalinks := extractPermalinks(content)

		switch {
		case firstSegment == ri.targetLang:
			trimmedRel := strings.TrimPrefix(relPath, firstSegment+"/")
			addRouteCandidates(ri.localizedRoutes, trimmedRel, permalinks)
		case ri.isLocalePrefix(firstSegment):
			return nil
		default:
			addRouteCandidates(ri.sourceRoutes, relPath, permalinks)
		}
		return nil
	})
}

func discoverLocalePrefixes(docsRoot string) (map[string]struct{}, error) {
	entries, err := os.ReadDir(docsRoot)
	if err != nil {
		return nil, err
	}
	locales := map[string]struct{}{}
	for _, entry := range entries {
		if !entry.IsDir() {
			continue
		}
		name := entry.Name()
		if !localeDirRe.MatchString(name) {
			continue
		}
		locales[name] = struct{}{}
	}
	return locales, nil
}

func isMarkdownFile(path string) bool {
	return strings.HasSuffix(path, ".md") || strings.HasSuffix(path, ".mdx")
}

func normalizeSlashes(path string) string {
	return strings.ReplaceAll(path, "\\", "/")
}

func firstPathSegment(relPath string) string {
	if relPath == "" {
		return ""
	}
	parts := strings.SplitN(relPath, "/", 2)
	return parts[0]
}

func addRouteCandidates(routes map[string]struct{}, relPath string, permalinks []string) {
	base := strings.TrimSuffix(strings.TrimSuffix(relPath, ".md"), ".mdx")
	if base != relPath {
		addRoute(routes, normalizeRoute(base))
		switch {
		case base == "index":
			addRoute(routes, "/")
		case strings.HasSuffix(base, "/index"):
			addRoute(routes, normalizeRoute(strings.TrimSuffix(base, "/index")))
		}
	}

	for _, permalink := range permalinks {
		addRoute(routes, normalizeRoute(permalink))
	}
}

func addRoute(routes map[string]struct{}, route string) {
	if route == "" {
		return
	}
	routes[route] = struct{}{}
}

func extractPermalinks(content []byte) []string {
	frontMatter, _ := splitFrontMatter(string(content))
	if strings.TrimSpace(frontMatter) == "" {
		return nil
	}

	data := map[string]any{}
	if err := yaml.Unmarshal([]byte(frontMatter), &data); err != nil {
		return nil
	}

	raw, ok := data["permalink"].(string)
	if !ok {
		return nil
	}
	permalink := strings.TrimSpace(raw)
	if permalink == "" {
		return nil
	}
	return []string{permalink}
}

func normalizeRoute(path string) string {
	trimmed := strings.TrimSpace(path)
	if trimmed == "" {
		return ""
	}
	stripped := strings.Trim(trimmed, "/")
	if stripped == "" {
		return "/"
	}
	return "/" + stripped
}

func (ri *routeIndex) localizeBodyLinks(body string) string {
	if ri == nil || ri.targetLang == "" || strings.EqualFold(ri.targetLang, "en") {
		return body
	}

	state := NewPlaceholderState(body)
	placeholders := make([]string, 0, 8)
	mapping := map[string]string{}
	masked := maskMatches(body, fencedBacktickCodeBlock, state.Next, &placeholders, mapping)
	masked = maskMatches(masked, fencedTildeCodeBlock, state.Next, &placeholders, mapping)
	masked = maskMatches(masked, inlineCodeRe, state.Next, &placeholders, mapping)

	masked = rewriteMarkdownLinkTargets(masked, ri)
	masked = rewriteHrefTargets(masked, ri)

	return unmaskMarkdown(masked, placeholders, mapping)
}

func rewriteMarkdownLinkTargets(text string, ri *routeIndex) string {
	matches := markdownLinkTargetRe.FindAllStringSubmatchIndex(text, -1)
	if len(matches) == 0 {
		return text
	}

	var out strings.Builder
	pos := 0
	for _, span := range matches {
		fullStart, targetStart, targetEnd := span[0], span[2], span[3]
		if fullStart < pos {
			continue
		}

		out.WriteString(text[pos:targetStart])
		target := text[targetStart:targetEnd]
		if text[fullStart] == '!' {
			out.WriteString(target)
		} else {
			out.WriteString(ri.localizeURL(target))
		}
		pos = targetEnd
	}
	out.WriteString(text[pos:])
	return out.String()
}

func rewriteHrefTargets(text string, ri *routeIndex) string {
	text = rewriteCapturedTargets(text, hrefDoubleQuotedValueRe, 2, ri)
	text = rewriteCapturedTargets(text, hrefSingleQuotedValueRe, 2, ri)
	return text
}

func rewriteCapturedTargets(text string, re *regexp.Regexp, groupIndex int, ri *routeIndex) string {
	matches := re.FindAllStringSubmatchIndex(text, -1)
	if len(matches) == 0 {
		return text
	}

	var out strings.Builder
	pos := 0
	for _, span := range matches {
		start, end := span[groupIndex], span[groupIndex+1]
		if start < pos || start < 0 || end < 0 {
			continue
		}
		out.WriteString(text[pos:start])
		out.WriteString(ri.localizeURL(text[start:end]))
		pos = end
	}
	out.WriteString(text[pos:])
	return out.String()
}

func (ri *routeIndex) localizeURL(raw string) string {
	trimmed := strings.TrimSpace(raw)
	if trimmed == "" {
		return raw
	}
	if strings.HasPrefix(trimmed, "#") || strings.HasPrefix(trimmed, "//") {
		return raw
	}
	if hasURLScheme(trimmed) {
		return raw
	}

	pathPart, suffix := splitURLSuffix(trimmed)
	if !strings.HasPrefix(pathPart, "/") {
		return raw
	}

	normalized := normalizeRoute(pathPart)
	if ri.routeHasLocalePrefix(normalized) {
		return raw
	}

	canonical, ok := ri.resolveRoute(normalized)
	if !ok {
		return raw
	}
	if _, ok := ri.localizedRoutes[canonical]; !ok {
		return raw
	}

	return prefixLocaleRoute(ri.targetLang, canonical) + suffix
}

func hasURLScheme(raw string) bool {
	switch {
	case strings.HasPrefix(raw, "http://"), strings.HasPrefix(raw, "https://"):
		return true
	case strings.HasPrefix(raw, "mailto:"), strings.HasPrefix(raw, "tel:"):
		return true
	case strings.HasPrefix(raw, "data:"), strings.HasPrefix(raw, "javascript:"):
		return true
	default:
		return false
	}
}

func splitURLSuffix(raw string) (string, string) {
	index := strings.IndexAny(raw, "?#")
	if index == -1 {
		return raw, ""
	}
	return raw[:index], raw[index:]
}

func prefixLocaleRoute(lang, route string) string {
	if route == "/" {
		return "/" + lang
	}
	return "/" + lang + route
}

func (ri *routeIndex) routeHasLocalePrefix(route string) bool {
	if route == "/" {
		return false
	}
	firstSegment := strings.TrimPrefix(route, "/")
	firstSegment = strings.SplitN(firstSegment, "/", 2)[0]
	return ri.isLocalePrefix(firstSegment)
}

func (ri *routeIndex) isLocalePrefix(segment string) bool {
	if segment == "" {
		return false
	}
	_, ok := ri.localePrefixes[segment]
	return ok
}

func (ri *routeIndex) resolveRoute(route string) (string, bool) {
	current := normalizeRoute(route)
	if current == "" {
		return "", false
	}

	seen := map[string]struct{}{current: {}}
	for {
		next, ok := ri.redirects[current]
		if !ok {
			break
		}
		current = next
		if _, ok := seen[current]; ok {
			return "", false
		}
		seen[current] = struct{}{}
	}

	if current == "/" {
		_, ok := ri.localizedRoutes[current]
		return current, ok
	}
	if _, ok := ri.sourceRoutes[current]; ok {
		return current, true
	}
	if _, ok := ri.localizedRoutes[current]; ok {
		return current, true
	}
	return "", false
}
