package main

import (
	"bytes"
	"net/url"
	"os"
	"path/filepath"
	"regexp"
	"strings"

	"github.com/yuin/goldmark"
	"github.com/yuin/goldmark/ast"
	"github.com/yuin/goldmark/parser"
	"github.com/yuin/goldmark/renderer"
	"github.com/yuin/goldmark/text"
	"github.com/yuin/goldmark/util"
)

// wikiLinkRe matches [[target]] and [[target::display]] anchored at the start
// of the current inline reader. Target and display forbid newlines and
// brackets; the target is non-greedy so the first `::` separates target from
// display when both are present.
var wikiLinkRe = regexp.MustCompile(`^\[\[([^\[\]\n]+?)(?:::([^\[\]\n]+))?\]\]`)

// wikiLinkPattern matches wiki-link tokens anywhere in a markdown source.
// Used by the move-endpoint rewriter; not by the goldmark parser.
var wikiLinkPattern = regexp.MustCompile(`\[\[([^\[\]\n]+?)(?:::([^\[\]\n]+))?\]\]`)

// wikiLinkNode is the AST node produced by wikiLinkParser.
type wikiLinkNode struct {
	ast.BaseInline
	Target  []byte
	Display []byte
}

var kindWikiLink = ast.NewNodeKind("WikiLink")

func (n *wikiLinkNode) Kind() ast.NodeKind { return kindWikiLink }

func (n *wikiLinkNode) Dump(source []byte, level int) {
	ast.DumpHelper(n, source, level, map[string]string{
		"Target":  string(n.Target),
		"Display": string(n.Display),
	}, nil)
}

// isValidWikiTarget rejects targets that are not absolute or that contain
// traversal / empty segments. Matches the validation used by the move endpoint.
func isValidWikiTarget(target []byte) bool {
	if len(target) == 0 || target[0] != '/' {
		return false
	}
	trimmed := strings.Trim(string(target), "/")
	if trimmed == "" {
		return true // root link
	}
	for _, seg := range strings.Split(trimmed, "/") {
		if seg == "" || seg == "." || seg == ".." {
			return false
		}
	}
	return true
}

type wikiLinkParser struct{}

func (p *wikiLinkParser) Trigger() []byte { return []byte{'['} }

func (p *wikiLinkParser) Parse(parent ast.Node, block text.Reader, pc parser.Context) ast.Node {
	line, _ := block.PeekLine()
	if len(line) < 4 || line[0] != '[' || line[1] != '[' {
		return nil
	}
	m := wikiLinkRe.FindSubmatchIndex(line)
	if m == nil {
		return nil
	}
	target := bytes.TrimSpace(line[m[2]:m[3]])
	if !isValidWikiTarget(target) {
		return nil
	}
	var display []byte
	if m[4] != -1 {
		display = bytes.TrimSpace(line[m[4]:m[5]])
	}
	block.Advance(m[1])
	return &wikiLinkNode{
		Target:  append([]byte(nil), target...),
		Display: append([]byte(nil), display...),
	}
}

// normalizeWikiTarget strips a trailing slash (but leaves "/" intact) and
// returns the cleaned absolute path.
func normalizeWikiTarget(target string) string {
	if target == "/" {
		return "/"
	}
	return "/" + strings.Trim(target, "/")
}

// wikiTargetHref converts a wiki target to a URL href with each segment
// percent-encoded and a trailing slash appended.
func wikiTargetHref(target string) string {
	target = normalizeWikiTarget(target)
	if target == "/" {
		return "/"
	}
	var b strings.Builder
	for _, seg := range strings.Split(strings.TrimPrefix(target, "/"), "/") {
		b.WriteByte('/')
		b.WriteString(url.PathEscape(seg))
	}
	b.WriteByte('/')
	return b.String()
}

// wikiTargetExists reports whether the on-disk path backing the target exists
// under root. Any existing path — file or folder — counts as resolved; only a
// missing path is treated as broken.
func wikiTargetExists(root, target string) bool {
	target = normalizeWikiTarget(target)
	fsPath := filepath.Join(root, filepath.FromSlash(strings.TrimPrefix(target, "/")))
	_, err := os.Stat(fsPath)
	return err == nil
}

// wikiDefaultDisplay returns the last segment of a target, or "/" for the root.
func wikiDefaultDisplay(target string) string {
	target = normalizeWikiTarget(target)
	if target == "/" {
		return "/"
	}
	segs := strings.Split(strings.TrimPrefix(target, "/"), "/")
	return segs[len(segs)-1]
}

type wikiLinkRenderer struct {
	root string
}

func (r *wikiLinkRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
	reg.Register(kindWikiLink, r.render)
}

func (r *wikiLinkRenderer) render(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
	if !entering {
		return ast.WalkContinue, nil
	}
	n := node.(*wikiLinkNode)
	target := string(n.Target)
	href := wikiTargetHref(target)
	display := string(n.Display)
	if display == "" {
		display = wikiDefaultDisplay(target)
	}
	broken := !wikiTargetExists(r.root, target)

	w.WriteString(`<a href="`)
	w.WriteString(href)
	w.WriteString(`"`)
	if broken {
		w.WriteString(` class="broken"`)
	}
	w.WriteString(`>`)
	w.Write(util.EscapeHTML([]byte(display)))
	w.WriteString(`</a>`)
	return ast.WalkContinue, nil
}

type wikiLinkExt struct{ root string }

// newWikiLinkExt returns a goldmark extension that turns [[...]] tokens into
// links resolved against root.
func newWikiLinkExt(root string) goldmark.Extender {
	return &wikiLinkExt{root: root}
}

func (e *wikiLinkExt) Extend(m goldmark.Markdown) {
	// Priority 199 — one higher than the default link parser (200) so
	// [[...]] is consumed before the default parser sees the outer `[`.
	m.Parser().AddOptions(parser.WithInlineParsers(
		util.Prioritized(&wikiLinkParser{}, 199),
	))
	m.Renderer().AddOptions(renderer.WithNodeRenderers(
		util.Prioritized(&wikiLinkRenderer{root: e.root}, 500),
	))
}
