package git

import (
	"bytes"
	"errors"
	"fmt"
	"io"
	"log/slog"
	"net/http"
	"os"
	"path/filepath"
	"strings"

	"git.gabrielgio.me/cerrado/pkg/ext"
	"git.gabrielgio.me/cerrado/pkg/service"
	"git.gabrielgio.me/cerrado/pkg/u"
	"git.gabrielgio.me/cerrado/templates"
	"github.com/alecthomas/chroma/v2"
	"github.com/alecthomas/chroma/v2/formatters/html"
	"github.com/alecthomas/chroma/v2/lexers"
	"github.com/alecthomas/chroma/v2/styles"
	"github.com/go-git/go-git/v5/plumbing/object"
	"github.com/gomarkdown/markdown"
	markdownhtml "github.com/gomarkdown/markdown/html"
	"github.com/gomarkdown/markdown/parser"
)

type (
	GitHandler struct {
		gitService *service.GitService
		config     configurationRepository
	}

	configurationRepository interface {
		GetRootReadme() string
		GetSyntaxHighlight() string
	}
)

func NewGitHandler(gitService *service.GitService, confRepo configurationRepository) *GitHandler {
	return &GitHandler{
		gitService: gitService,
		config:     confRepo,
	}
}

func (g *GitHandler) List(w http.ResponseWriter, r *http.Request) error {
	// this is the only handler that needs to handle authentication itself.
	// everything else relay on name path parameter
	logged := ext.IsLoggedIn(r.Context())

	repos, err := g.gitService.ListRepositories()
	if err != nil {
		return err
	}

	if !logged {
		repos = u.Filter(repos, isPublic)
	}

	f, err := os.Open(g.config.GetRootReadme())
	if err != nil {
		return err
	}

	bs, err := io.ReadAll(f)
	if err != nil {
		return err
	}

	extensions := parser.CommonExtensions | parser.AutoHeadingIDs | parser.NoEmptyLineBeforeBlock
	p := parser.NewWithExtensions(extensions)
	doc := p.Parse(bs)

	htmlFlag := markdownhtml.CommonFlags | markdownhtml.HrefTargetBlank
	opts := markdownhtml.RendererOptions{Flags: htmlFlag}
	renderer := markdownhtml.NewRenderer(opts)

	bs = markdown.Render(doc, renderer)

	gitList := &templates.GitListPage{
		Respositories: repos,
		About:         bs,
	}
	templates.WritePageTemplate(w, gitList, r.Context())
	return nil
}

func (g *GitHandler) Archive(w http.ResponseWriter, r *http.Request) error {
	ext.SetGZip(w)
	name := r.PathValue("name")
	file := r.PathValue("file")
	ref := strings.TrimSuffix(file, ".tar.gz")

	// TODO: remove it once we can support more than gzip
	if !strings.HasSuffix(file, ".tar.gz") {
		ext.NotFound(w, r)
		return nil
	}

	filename := fmt.Sprintf("%s-%s.tar.gz", name, ref)
	ext.SetFileName(w, filename)

	prefix := fmt.Sprintf("%s-%s", name, ref)
	err := g.gitService.WriteTarGZip(w, name, ref, prefix)
	if err != nil {
		// once we start writing to the body we can't report error anymore
		// so we are only left with printing the error.
		slog.Error("Error generating tar gzip file", "error", err)
	}

	return nil
}

func (g *GitHandler) Summary(w http.ResponseWriter, r *http.Request) error {
	ext.SetHTML(w)
	name := r.PathValue("name")
	ref, err := g.gitService.GetHead(name)
	if err != nil {
		return err
	}

	tags, err := g.gitService.ListTags(name)
	if err != nil {
		return err
	}

	branches, err := g.gitService.ListBranches(name)
	if err != nil {
		return err
	}

	commits, err := g.gitService.ListCommits(name, "", 10)
	if err != nil {
		return err
	}

	gitList := &templates.GitItemPage{
		Name: name,
		Ref:  ref.Name().Short(),
		GitItemBase: &templates.GitItemSummaryPage{
			Tags:     tags,
			Branches: branches,
			Commits:  commits,
		},
	}
	templates.WritePageTemplate(w, gitList, r.Context())
	return nil
}

func (g *GitHandler) About(w http.ResponseWriter, r *http.Request) error {
	ext.SetHTML(w)
	name := r.PathValue("name")
	ref, err := g.gitService.GetHead(name)
	if err != nil {
		return err
	}

	file, err := g.gitService.GetAbout(name)
	if errors.Is(err, object.ErrFileNotFound) {
		templates.WritePageTemplate(w, &templates.GitItemPage{
			Name: name,
			Ref:  ref.Name().Short(),
			GitItemBase: &templates.GitItemAboutPage{
				About: []byte("About file not configured properly"),
			},
		}, r.Context())
		return nil
	}
	if err != nil {
		return err
	}

	extensions := parser.CommonExtensions | parser.AutoHeadingIDs | parser.NoEmptyLineBeforeBlock
	p := parser.NewWithExtensions(extensions)
	doc := p.Parse(file)

	htmlFlag := markdownhtml.CommonFlags | markdownhtml.HrefTargetBlank
	opts := markdownhtml.RendererOptions{Flags: htmlFlag}
	renderer := markdownhtml.NewRenderer(opts)

	bs := markdown.Render(doc, renderer)

	gitList := &templates.GitItemPage{
		Name: name,
		Ref:  ref.Name().Short(),
		GitItemBase: &templates.GitItemAboutPage{
			About: bs,
		},
	}
	templates.WritePageTemplate(w, gitList, r.Context())
	return nil
}

func (g *GitHandler) Refs(w http.ResponseWriter, r *http.Request) error {
	ext.SetHTML(w)
	name := r.PathValue("name")

	tags, err := g.gitService.ListTags(name)
	if err != nil {
		return err
	}

	branches, err := g.gitService.ListBranches(name)
	if err != nil {
		return err
	}

	ref, err := g.gitService.GetHead(name)
	if err != nil {
		return err
	}

	gitList := &templates.GitItemPage{
		Name: name,
		Ref:  ref.Name().Short(),
		GitItemBase: &templates.GitItemRefsPage{
			Tags:     tags,
			Branches: branches,
		},
	}
	templates.WritePageTemplate(w, gitList, r.Context())
	return nil
}

func (g *GitHandler) Tree(w http.ResponseWriter, r *http.Request) error {
	ext.SetHTML(w)
	name := r.PathValue("name")
	ref := r.PathValue("ref")
	rest := r.PathValue("rest")
	paths := []string{}

	// this is avoid Split from generating a len 1 array with empty string
	if rest != "" {
		paths = strings.Split(rest, "/")
	}

	tree, err := g.gitService.GetTree(name, ref, rest)
	if err != nil {
		return err
	}

	gitList := &templates.GitItemPage{
		Name: name,
		Ref:  ref,
		GitItemBase: &templates.GitItemTreePage{
			Path: paths,
			Tree: tree,
		},
	}
	templates.WritePageTemplate(w, gitList, r.Context())
	return nil
}

func (g *GitHandler) Blob(w http.ResponseWriter, r *http.Request) error {
	ext.SetHTML(w)
	name := r.PathValue("name")
	ref := r.PathValue("ref")
	rest := r.PathValue("rest")
	paths := []string{}

	// this is avoid Split from generating a len 1 array with empty string
	if rest != "" {
		paths = strings.Split(rest, "/")
	}

	isBin, err := g.gitService.IsBinary(name, ref, rest)
	if err != nil {
		return err
	}

	// if it is binary no need to over all the chroma process
	if isBin {
		gitList := &templates.GitItemPage{
			Name: name,
			Ref:  ref,
			GitItemBase: &templates.GitItemBlobPage{
				Path:    paths,
				Content: []byte("Binary file"),
			},
		}
		templates.WritePageTemplate(w, gitList, r.Context())
		return nil
	}

	file, err := g.gitService.GetFileContent(name, ref, rest)
	if err != nil {
		return err
	}

	filename := filepath.Base(rest)
	lexer := GetLexers(filename)
	style := styles.Get(g.config.GetSyntaxHighlight())

	formatter := html.New(
		html.WithLineNumbers(true),
		html.WithLinkableLineNumbers(true, "L"),
	)

	iterator, err := lexer.Tokenise(nil, string(file))
	if err != nil {
		return err
	}

	var code bytes.Buffer
	err = formatter.Format(&code, style, iterator)
	if err != nil {
		return err
	}

	gitList := &templates.GitItemPage{
		Name: name,
		Ref:  ref,
		GitItemBase: &templates.GitItemBlobPage{
			Path:    paths,
			Content: code.Bytes(),
		},
	}
	templates.WritePageTemplate(w, gitList, r.Context())
	return nil
}

func (g *GitHandler) Log(w http.ResponseWriter, r *http.Request) error {
	ext.SetHTML(w)
	name := r.PathValue("name")
	ref := r.PathValue("ref")

	commits, err := g.gitService.ListCommits(name, ref, 1000)
	if err != nil {
		return err
	}

	gitList := &templates.GitItemPage{
		Name: name,
		Ref:  ref,
		GitItemBase: &templates.GitItemLogPage{
			Commits: commits,
		},
	}
	templates.WritePageTemplate(w, gitList, r.Context())
	return nil
}

func (g *GitHandler) Commit(w http.ResponseWriter, r *http.Request) error {
	ext.SetHTML(w)
	name := r.PathValue("name")
	ref := r.PathValue("ref")

	commit, err := g.gitService.LastCommit(name, ref)
	if err != nil {
		return err
	}

	diff, err := g.gitService.Diff(name, ref)
	if err != nil {
		return err
	}

	gitList := &templates.GitItemPage{
		Name: name,
		Ref:  ref,
		GitItemBase: &templates.GitItemCommitPage{
			Commit: commit,
			Diff:   diff,
		},
	}
	templates.WritePageTemplate(w, gitList, r.Context())
	return nil
}

func GetLexers(filename string) chroma.Lexer {
	if filename == "APKBUILD" {
		return lexers.Get("sh")
	}

	if strings.HasSuffix(filename, ".qtpl") {
		return lexers.Get("html")
	}

	lexer := lexers.Get(filename)

	if lexer == nil {
		lexer = lexers.Get("txt")
	}
	return lexer
}

func isPublic(r *service.Repository) bool {
	return r.Public
}