package normalizer

import (
	"context"
	"net/url"
	"path"
	"slices"
	"strconv"
	"strings"

	"github.com/bluesky-social/indigo/atproto/identity"
	"github.com/bluesky-social/indigo/atproto/syntax"
	"github.com/yuin/goldmark"
	"github.com/yuin/goldmark/ast"
	"github.com/yuin/goldmark/extension"
	"github.com/yuin/goldmark/parser"
	"github.com/yuin/goldmark/text"
	"tangled.org/core/appview/db"
	"tangled.org/core/appview/models"
	textension "tangled.org/core/appview/pages/markup/extension"
	"tangled.org/core/sets"
)

type MarkdownNormalizer struct {
	hostname string
	dir      identity.Directory
	db       db.Execer
}

func NewMarkdownNormalizer(hostname string, dir identity.Directory) *MarkdownNormalizer {
	return &MarkdownNormalizer{
		hostname: hostname,
		dir:      dir,
	}
}

var markdownParser parser.Parser

func init() {
	markdownParser = goldmark.New(
		goldmark.WithExtensions(
			extension.GFM,
			extension.NewFootnote(
				extension.WithFootnoteIDPrefix([]byte("footnote")),
			),
			textension.AtExt,
		),
	).Parser()
}

// 1. parse markdown with AtExt
// 2. walk AST
//   - normalize AtExt segments -> [did:...] (when resolvable)
//     @alice.com -> [did:plc:alice]
//   - normalize Tangled-Link segments
//     <https://...> -> `[at://...]`

// from render step:
// 1. parse normalized version with full-parser & custom parser.Context
//    custom parser.Context will resolve custom ref-links and collect them into mentions & references
// 2. with resolved links, start render phase. we should know all at-uris at this phase.

// Normalize normalizes markdown body
//
//	@alice.com -> [did:plc:alice]
//	#123 -> [at://...]
//	[link](https://tangled.org/...) -> [link](at://...)
//
// NOTE: last two cases are not implemented yet.
func (m *MarkdownNormalizer) Normalize(ctx context.Context, source []byte) ([]byte, []syntax.DID, []syntax.ATURI) {
	var (
		normalizedSource []byte
		mentions         = sets.New[syntax.DID]()
		rawRefLinks      = sets.New[models.ReferenceLink]()
	)

	root := markdownParser.Parse(text.NewReader(source))
	cursor := 0
	ast.Walk(root, func(n ast.Node, entering bool) (ast.WalkStatus, error) {
		if !entering {
			return ast.WalkContinue, nil
		}

		switch n.Kind() {
		case textension.KindAt:
			node := n.(*textension.AtNode)

			start := node.Segment.Start
			stop := node.Segment.Stop

			if cursor > start {
				break
			}

			ident, err := m.dir.LookupHandle(ctx, syntax.Handle(node.Handle))
			if err != nil {
				return ast.WalkContinue, nil
			}

			mentions.Insert(ident.DID)

			normalizedSource = append(normalizedSource, source[cursor:start]...)
			normalizedSource = append(normalizedSource, byte('['))
			normalizedSource = append(normalizedSource, []byte(ident.DID.String())...)
			normalizedSource = append(normalizedSource, byte(']'))

			cursor = stop
			return ast.WalkSkipChildren, nil

			// case ast.HashRef:
			// 	node := n.(*textension.HashRefNode)
			// 	// TODO: normalize to at-uri
			// 	// example: [at://...]
			// 	return ast.WalkSkipChildren, nil

		case ast.KindLink:
			node := n.(*ast.Link)
			dest := string(node.Destination)
			if ref := parseTangledLink(m.hostname, dest); ref != nil {
				rawRefLinks.Insert(*ref)
			}

			// TODO: convert dest to at-uri
			// example: [link](at://...)

			return ast.WalkSkipChildren, nil

		case ast.KindAutoLink:
			node := n.(*ast.AutoLink)
			if node.AutoLinkType == ast.AutoLinkURL {
				dest := string(node.URL(source))
				if ref := parseTangledLink(m.hostname, dest); ref != nil {
					rawRefLinks.Insert(*ref)
				}
			}

			// TODO: convert dest to at-uri
			// example: <at://...>

			return ast.WalkSkipChildren, nil
		}

		return ast.WalkContinue, nil
	})
	if cursor < len(source) {
		normalizedSource = append(normalizedSource, source[cursor:]...)
	}

	var resolvedRefs []models.ReferenceLink
	for rawRef := range rawRefLinks.All() {
		ident, err := m.dir.Lookup(ctx, syntax.AtIdentifier(rawRef.Handle))
		if err != nil || ident == nil {
			continue
		}
		rawRef.Handle = string(ident.DID)
		resolvedRefs = append(resolvedRefs, rawRef)
	}

	// TODO: ideally tangled-uri should be universally resolvable and should not require DB query.
	// We are querying DB here because issue/pull-ids are not decentralized yet.
	references, err := db.ValidateReferenceLinks(m.db, resolvedRefs)
	if err != nil {
	}

	return normalizedSource, slices.Collect(mentions.All()), references
}

func parseTangledLink(baseHost string, urlStr string) *models.ReferenceLink {
	u, err := url.Parse(urlStr)
	if err != nil {
		return nil
	}
	if u.Host != "" && !strings.EqualFold(u.Host, baseHost) {
		return nil
	}

	p := path.Clean(u.Path)
	parts := strings.FieldsFunc(p, func(r rune) bool { return r == '/' })
	if len(parts) < 4 {
		// need at least: atIdentifier / repoName / kind / id
		return nil
	}

	var (
		atIdentifier = parts[0]
		repoName     = parts[1]
		kindSeg      = parts[2]
		subjectSeg   = parts[3]
	)

	atIdentifier = strings.TrimPrefix(atIdentifier, "@")

	var kind models.RefKind
	switch kindSeg {
	case "issues":
		kind = models.RefKindIssue
	case "pulls":
		kind = models.RefKindPull
	default:
		return nil
	}

	subjectId, err := strconv.Atoi(subjectSeg)
	if err != nil {
		return nil
	}
	var commentId *int
	if u.Fragment != "" {
		if strings.HasPrefix(u.Fragment, "comment-") {
			commentIdStr := u.Fragment[len("comment-"):]
			if id, err := strconv.Atoi(commentIdStr); err == nil {
				commentId = &id
			}
		}
	}

	return &models.ReferenceLink{
		Handle:    atIdentifier,
		Repo:      repoName,
		Kind:      kind,
		SubjectId: subjectId,
		CommentId: commentId,
	}
}
