package web

import (
	"bytes"
	"context"
	"html/template"
	"strings"
	"sync"

	"github.com/charmbracelet/soft-serve/git"
	"github.com/charmbracelet/soft-serve/pkg/backend"
	"github.com/charmbracelet/soft-serve/pkg/proto"
	"github.com/microcosm-cc/bluemonday"
	"github.com/yuin/goldmark"
	extension "github.com/yuin/goldmark/extension"
	"github.com/yuin/goldmark/parser"
	goldmarkhtml "github.com/yuin/goldmark/renderer/html"
	"github.com/yuin/goldmark/util"
)

var (
	sanitizerPolicy     *bluemonday.Policy
	sanitizerPolicyOnce sync.Once
)

// getSanitizerPolicy returns a cached sanitizer policy.
// The policy is safe for concurrent use after initialization.
// Note: Tests should not use t.Parallel() with functions that call renderMarkdown
// until we verify bluemonday.Policy initialization is thread-safe during sync.Once.
func getSanitizerPolicy() *bluemonday.Policy {
	sanitizerPolicyOnce.Do(func() {
		policy := bluemonday.NewPolicy()

		// Allowed tags
		policy.AllowElements("p", "h1", "h2", "h3", "h4", "h5", "h6",
			"ul", "ol", "li", "pre", "code", "blockquote",
			"strong", "em", "del", "br", "hr",
			"table", "thead", "tbody", "tr", "th", "td",
			"center", "a", "img", "details", "summary")

		// Allow id attributes on headings (generated by AutoHeadingID for anchor links)
		policy.AllowAttrs("id").OnElements("h1", "h2", "h3", "h4", "h5", "h6")

		// Links: only https scheme and relative URLs, add nofollow/noreferrer
		policy.AllowAttrs("href").OnElements("a")
		policy.RequireNoFollowOnLinks(true)
		policy.RequireNoReferrerOnLinks(true)

		// Images: only https scheme or repo-relative paths, allow descriptive attrs
		policy.AllowAttrs("src", "alt", "title", "width", "height").OnElements("img")

		// URL schemes: only HTTPS for external URLs, relative URLs for repo paths
		policy.AllowURLSchemes("https")
		policy.AllowRelativeURLs(true)

		// Table alignment attributes
		policy.AllowAttrs("align").Matching(bluemonday.SpaceSeparatedTokens).OnElements("th", "td")

		sanitizerPolicy = policy
	})
	return sanitizerPolicy
}

// renderMarkdown converts markdown content to sanitized HTML.
// If ctx is provided, relative URLs will be rewritten to point to repository files.
func renderMarkdown(content []byte, ctx *ReadmeContext) (template.HTML, error) {
	var buf bytes.Buffer

	mdOpts := []goldmark.Option{
		goldmark.WithExtensions(extension.GFM),
		goldmark.WithParserOptions(
			parser.WithAutoHeadingID(),
		),
		goldmark.WithRendererOptions(
			goldmarkhtml.WithUnsafe(),
		),
	}

	// Add URL rewriter if context is provided
	if ctx != nil {
		rewriter := newURLRewriter(*ctx)
		mdOpts = append(mdOpts, goldmark.WithParserOptions(
			parser.WithASTTransformers(util.Prioritized(rewriter, 500)),
		))
	}

	md := goldmark.New(mdOpts...)

	if err := md.Convert(content, &buf); err != nil {
		return "", err
	}

	// Use cached sanitizer policy
	sanitized := getSanitizerPolicy().SanitizeBytes(buf.Bytes())

	return template.HTML(sanitized), nil
}

// getServerReadme loads and renders the README from the .soft-serve repository.
// Returns both the raw markdown content and the rendered HTML.
func getServerReadme(ctx context.Context, be *backend.Backend) (raw string, html template.HTML, err error) {
	repos, err := be.Repositories(ctx)
	if err != nil {
		return "", "", err
	}

	for _, r := range repos {
		if r.Name() == ".soft-serve" {
			readme, _, err := backend.Readme(r, nil)
			if err != nil {
				return "", "", err
			}
			if readme != "" {
				html, err := renderMarkdown([]byte(readme), nil)
				if err != nil {
					return "", "", err
				}
				return readme, html, nil
			}
		}
	}

	return "", "", nil
}

// openRepository opens a git repository.
func openRepository(repo proto.Repository) (*git.Repository, error) {
	return repo.Open()
}

// getDefaultBranch returns the default branch name, or empty string if none exists.
func getDefaultBranch(gr *git.Repository) string {
	head, err := gr.HEAD()
	if err != nil || head == nil {
		return ""
	}
	return head.Name().Short()
}

// truncateText truncates text to maxLength characters, respecting word boundaries.
// If truncated, appends "...". Returns empty string if text is empty or maxLength <= 0.
func truncateText(text string, maxLength int) string {
	text = strings.TrimSpace(text)
	if text == "" || maxLength <= 0 {
		return ""
	}

	if len(text) <= maxLength {
		return text
	}

	// Find last space before maxLength
	truncated := text[:maxLength]
	if lastSpace := strings.LastIndex(truncated, " "); lastSpace > 0 {
		truncated = truncated[:lastSpace]
	}

	return truncated + "..."
}

// extractPlainTextFromMarkdown converts markdown to plain text and truncates to maxLength.
// Strips markdown formatting to produce a clean description suitable for meta tags.
func extractPlainTextFromMarkdown(markdown string, maxLength int) string {
	markdown = strings.TrimSpace(markdown)
	if markdown == "" {
		return ""
	}

	// Simple markdown stripping - remove common formatting
	text := markdown

	// Remove headers
	text = strings.ReplaceAll(text, "# ", "")
	text = strings.ReplaceAll(text, "## ", "")
	text = strings.ReplaceAll(text, "### ", "")
	text = strings.ReplaceAll(text, "#### ", "")
	text = strings.ReplaceAll(text, "##### ", "")
	text = strings.ReplaceAll(text, "###### ", "")

	// Remove bold/italic markers
	text = strings.ReplaceAll(text, "**", "")
	text = strings.ReplaceAll(text, "__", "")
	text = strings.ReplaceAll(text, "*", "")
	text = strings.ReplaceAll(text, "_", "")

	// Remove code blocks and inline code
	text = strings.ReplaceAll(text, "`", "")

	// Remove links but keep text: [text](url) -> text
	for strings.Contains(text, "[") && strings.Contains(text, "](") {
		start := strings.Index(text, "[")
		mid := strings.Index(text[start:], "](")
		if mid == -1 {
			break
		}
		end := strings.Index(text[start+mid+2:], ")")
		if end == -1 {
			break
		}
		linkText := text[start+1 : start+mid]
		text = text[:start] + linkText + text[start+mid+2+end+1:]
	}

	// Replace multiple spaces/newlines with single space
	text = strings.Join(strings.Fields(text), " ")

	return truncateText(text, maxLength)
}

// getRepoDescriptionOrFallback returns the repository description if available,
// otherwise returns the fallback text. Result is truncated to 200 characters.
func getRepoDescriptionOrFallback(repo proto.Repository, fallback string) string {
	desc := strings.TrimSpace(repo.Description())
	if desc == "" {
		desc = fallback
	}
	return truncateText(desc, 200)
}
