package markup

import (
	"bytes"
	"context"
	"fmt"
	"io"
	"net/url"
	"strings"

	chromahtml "github.com/alecthomas/chroma/v2/formatters/html"
	"github.com/alecthomas/chroma/v2/styles"
	"github.com/bluesky-social/indigo/atproto/syntax"
	"github.com/ipfs/go-cid"
	"github.com/yuin/goldmark"
	emoji "github.com/yuin/goldmark-emoji"
	highlighting "github.com/yuin/goldmark-highlighting/v2"
	"github.com/yuin/goldmark/extension"
	"github.com/yuin/goldmark/parser"
	"github.com/yuin/goldmark/renderer/html"
	callout "gitlab.com/staticnoise/goldmark-callout"
	"go.abhg.dev/goldmark/mermaid"
	htmlparse "golang.org/x/net/html"
	"tangled.org/core/appview/config"
	textension "tangled.org/core/appview/pages/markup/extension"
	"tangled.org/core/blobstore"
)

// make more pure markdown renderer.

type RenderGitBlobContext struct {
	RepoDid    syntax.DID
	CurrentRev string
	CurrentDir string
}

// MarkdownRenderer can render user-owned markdown bodies like issue title/body.
type MarkdownRenderer struct {
	CamoUrl    string
	CamoSecret string
	Hostname   string
	blobStore  blobstore.BlobStore // to generate blob url
	markdown   goldmark.Markdown
}

func NewMarkdownRenderer(cfg *config.Config, blobStore blobstore.BlobStore) *MarkdownRenderer {
	markdown := goldmark.New(
		goldmark.WithExtensions(
			extension.GFM,
			&mermaid.Extender{
				RenderMode: mermaid.RenderModeClient,
				NoScript:   true,
			},
			highlighting.NewHighlighting(
				highlighting.WithFormatOptions(
					chromahtml.Standalone(false),
					chromahtml.WithClasses(true),
				),
				highlighting.WithCustomStyle(styles.Get("catppuccin-latte")),
			),
			extension.NewFootnote(
				extension.WithFootnoteIDPrefix([]byte("footnote")),
			),
			callout.CalloutExtention,
			textension.AtExt,
			textension.NewTangledLinkExt(cfg.Core.AppviewHost),
			emoji.Emoji,
		),
		goldmark.WithParserOptions(
			parser.WithAutoHeadingID(),
		),
		goldmark.WithRendererOptions(html.WithUnsafe()),
	)
	return &MarkdownRenderer{
		CamoUrl:    cfg.Camo.Host,
		CamoSecret: cfg.Camo.SharedSecret,
		Hostname:   cfg.Core.AppviewHost,
		blobStore:  blobStore,
		markdown:   markdown,
	}
}

type RenderMarkdownBodyParams struct {
	OwnerDid  syntax.DID // Content owner DID. Used to make blob links
	IsPreview bool
}

func (m *MarkdownRenderer) RenderMarkdownBody(params *RenderMarkdownBodyParams, source string) string {
	var buf bytes.Buffer
	if err := m.markdown.Convert([]byte(source), &buf); err != nil {
		return source
	}

	var processed strings.Builder
	if err := postProcessHTML(
		strings.NewReader(buf.String()),
		&processed,
		func(node *htmlparse.Node) { m.visitNode(params, node) },
	); err != nil {
		return source
	}

	return processed.String()
}

func (m *MarkdownRenderer) visitNode(params *RenderMarkdownBodyParams, node *htmlparse.Node) {
	switch node.Type {
	case htmlparse.ElementNode:
		switch node.Data {
		case "img", "source":
			isBlob := false
			for i, attr := range node.Attr {
				if attr.Key != "src" {
					continue
				}

				if rawCid, found := strings.CutPrefix(attr.Val, "blob://"); found {
					isBlob = true
					cid, err := cid.Parse(rawCid)
					if err != nil {
						continue // skip invalid cid
					}
					blobUrl, err := m.blobStore.MakeBlobUrl(context.TODO(), params.OwnerDid, cid)
					if err != nil {
						continue
					}
					attr.Val = blobUrl
					node.Attr[i] = attr
					continue
				}

				src, err := url.Parse(attr.Val)
				if err != nil {
					continue // skip invalid url
				}

				if src.IsAbs() {
					camoUrl, _ := url.Parse(m.CamoUrl)
					if camoUrl != nil && src.Host != m.Hostname && src.Host != camoUrl.Host {
						attr.Val = GenerateCamoURL(m.CamoUrl, m.CamoSecret, attr.Val)
						node.Attr[i] = attr
					}
				}
			}
			if params.IsPreview && isBlob {
				setAttr(node, "onerror", fmt.Sprintf("this.onerror=null;this.src=%q", "/static/blobpreviewplaceholder.png"))
			}
		}

		for n := node.FirstChild; n != nil; n = n.NextSibling {
			m.visitNode(params, n)
		}
	default:
	}
}

func setAttr(node *htmlparse.Node, key, val string) {
	for i, attr := range node.Attr {
		if attr.Key != key {
			continue
		}
		attr.Val = val
		node.Attr[i] = attr
		return
	}
	node.Attr = append(node.Attr, htmlparse.Attribute{
		Key: key,
		Val: val,
	})
}

func postProcessHTML(input io.Reader, output io.Writer, processFn func(*htmlparse.Node)) error {
	node, err := htmlparse.Parse(io.MultiReader(
		strings.NewReader("<html><body>"),
		input,
		strings.NewReader("</body></html>"),
	))
	if err != nil {
		return fmt.Errorf("failed to parse html: %w", err)
	}

	if node.Type == htmlparse.DocumentNode {
		node = node.FirstChild
	}

	processFn(node)

	newNodes := make([]*htmlparse.Node, 0, 5)

	if node.Data == "html" {
		node = node.FirstChild
		for node != nil && node.Data != "body" {
			node = node.NextSibling
		}
	}
	if node != nil {
		if node.Data == "body" {
			child := node.FirstChild
			for child != nil {
				newNodes = append(newNodes, child)
				child = child.NextSibling
			}
		} else {
			newNodes = append(newNodes, node)
		}
	}

	for _, node := range newNodes {
		if err := htmlparse.Render(output, node); err != nil {
			return fmt.Errorf("failed to render processed html: %w", err)
		}
	}

	return nil
}
