package handlers

import (
	"errors"
	"fmt"
	"html"
	"html/template"
	"log/slog"
	"net/http"
	"os"
	"path/filepath"
	"sort"
	"strings"
	"time"

	"olexsmir.xyz/mugit/internal/git"
	"olexsmir.xyz/mugit/internal/markdown"
)

type Meta struct {
	Title       string
	Description string
	Host        string
	IsEmpty     bool
	GoMod       bool
	SSHEnabled  bool
}

type RepoBase struct {
	Ref  string
	Desc string
}

type PageData[T any] struct {
	Meta     Meta
	RepoName string // empty for non-repo pages, needed for _head.html to  compile
	P        T
}

func (h *handlers) indexHandler(w http.ResponseWriter, r *http.Request) {
	repos, err := h.listPublicRepos()
	if err != nil {
		h.write500(w, err)
		return
	}
	h.templ(w, "index", h.pageData(nil, repos))
}

type RepoIndex struct {
	Desc              string
	SSHUser           string
	IsEmpty           bool
	Readme            template.HTML
	Ref               string
	Commits           []*git.Commit
	IsMirror          bool
	MirrorURL         string
	MirrorLastSync    time.Time
	MirrorLastChecked time.Time
}

func (h *handlers) repoIndexHandler(w http.ResponseWriter, r *http.Request) {
	repo, err := h.openPublicRepo(r.PathValue("name"), "")
	if err != nil {
		h.write404(w, r.URL.Path, err)
		return
	}

	desc, err := repo.Description()
	if err != nil {
		h.write500(w, err)
		return
	}

	p := RepoIndex{
		Desc:    desc,
		IsEmpty: repo.IsEmpty(),
		SSHUser: h.c.SSH.User,
	}

	if isMirror, merr := repo.IsMirror(); isMirror && merr == nil {
		p.IsMirror = true
		p.MirrorURL, _ = repo.RemoteURL()
		p.MirrorLastSync, _ = repo.LastSync()
		p.MirrorLastChecked, _ = repo.LastChecked()
	}

	if p.IsEmpty {
		h.templ(w, "repo_index", h.pageData(repo, p))
		return
	}

	p.Ref, err = repo.DefaultBranch()
	if err != nil {
		h.write500(w, err)
		return
	}

	p.Readme, err = h.renderReadme(repo, p.Ref, "")
	if err != nil {
		h.write500(w, err)
		return
	}

	p.Commits, err = repo.Commits("")
	if err != nil {
		h.write500(w, err)
		return
	}

	if len(p.Commits) >= 3 {
		p.Commits = p.Commits[:3:3]
	}

	h.templ(w, "repo_index", h.pageData(repo, p))
}

type RepoTree struct {
	Desc        string
	Ref         string
	Tree        []git.NiceTree
	Breadcrumbs []Breadcrumb
	ParentPath  string
	DotDot      string
	Readme      template.HTML
}

func (h *handlers) repoTreeHandler(w http.ResponseWriter, r *http.Request) {
	name := r.PathValue("name")
	ref := h.parseRef(r.PathValue("ref"))
	treePath := r.PathValue("rest")

	repo, err := h.openPublicRepo(name, ref)
	if err != nil {
		h.write404(w, r.URL.Path, err)
		return
	}

	desc, err := repo.Description()
	if err != nil {
		h.write500(w, err)
		return
	}

	tree, err := repo.FileTree(r.Context(), treePath)
	if err != nil {
		h.write500(w, err)
		return
	}

	readme, err := h.renderReadme(repo, ref, treePath)
	if err != nil {
		h.write500(w, err)
		return
	}

	h.templ(w, "repo_tree", h.pageData(repo, RepoTree{
		Desc:        desc,
		Ref:         ref,
		Tree:        tree,
		ParentPath:  treePath,
		DotDot:      filepath.Dir(treePath),
		Breadcrumbs: Breadcrumbs(treePath),
		Readme:      readme,
	}))
}

type RepoFile struct {
	Ref         string
	Desc        string
	Lines       []string
	LastCommit  *git.Commit
	Breadcrumbs []Breadcrumb
	Path        string
	IsImage     bool
	IsBinary    bool
	Mime        string
	Size        int64
}

func (h *handlers) fileContentsHandler(w http.ResponseWriter, r *http.Request) {
	name := r.PathValue("name")
	ref := h.parseRef(r.PathValue("ref"))
	treePath := r.PathValue("rest")

	repo, err := h.openPublicRepo(name, ref)
	if err != nil {
		h.write404(w, r.URL.Path, err)
		return
	}

	fc, err := repo.FileContent(treePath)
	if err != nil {
		if errors.Is(err, git.ErrFileNotFound) {
			h.write404(w, r.URL.Path, err)
			return
		}
		h.write500(w, err)
		return
	}

	p := RepoFile{
		Ref:      ref,
		Path:     treePath,
		IsImage:  fc.IsImage,
		IsBinary: fc.IsBinary,
		Mime:     fc.Mime,
		Size:     fc.Size,
	}

	p.LastCommit, err = repo.LastFileCommit(r.Context(), treePath)
	if err != nil {
		h.write500(w, err)
		return
	}

	p.Desc, err = repo.Description()
	if err != nil {
		h.write500(w, err)
		return
	}

	p.Breadcrumbs = Breadcrumbs(treePath)
	if !fc.IsImage && !fc.IsBinary {
		p.Lines = strings.Split(strings.TrimRight(fc.String(), "\n"), "\n")
	}

	h.templ(w, "repo_file", h.pageData(repo, p))
}

func (h *handlers) rawFileContentsHandler(w http.ResponseWriter, r *http.Request) {
	name := r.PathValue("name")
	ref := h.parseRef(r.PathValue("ref"))
	treePath := r.PathValue("rest")

	repo, err := h.openPublicRepo(name, ref)
	if err != nil {
		w.WriteHeader(http.StatusNotFound)
		slog.Info("404", "err", err)
		return
	}

	fc, err := repo.FileContent(treePath)
	if err != nil {
		if errors.Is(err, git.ErrFileNotFound) {
			w.WriteHeader(http.StatusNotFound)
			slog.Info("404", "err", err)
			return
		}

		w.WriteHeader(http.StatusInternalServerError)
		slog.Info("500", "err", err)
		return
	}

	w.Header().Set("Content-Type", fc.Mime)
	w.Header().Set("Content-Disposition", "attachment")
	w.Header().Set("X-Content-Type-Options", "nosniff")
	w.WriteHeader(http.StatusOK)
	_, _ = w.Write(fc.Content)
}

type RepoLog struct {
	Desc      string
	Commits   []*git.Commit
	Ref       string
	NextAfter string
}

func (h *handlers) logHandler(w http.ResponseWriter, r *http.Request) {
	name := r.PathValue("name")
	ref := h.parseRef(r.PathValue("ref"))
	after := r.URL.Query().Get("after")

	repo, err := h.openPublicRepo(name, ref)
	if err != nil {
		h.write404(w, r.URL.Path, err)
		return
	}

	desc, err := repo.Description()
	if err != nil {
		h.write500(w, err)
		return
	}

	commits, err := repo.Commits(after)
	if err != nil {
		h.write500(w, err)
		return
	}

	// if we got full page of commits, we probably have more.
	// NOTE: this has an edge case, when last page is len(git.CommitsPage), "load more" would be shown
	nextAfter := ""
	if len(commits) == git.CommitsPage && len(commits) > 0 {
		nextAfter = commits[len(commits)-1].HashShort
	}

	h.templ(w, "repo_log", h.pageData(repo, RepoLog{
		Desc:      desc,
		Ref:       ref,
		Commits:   commits,
		NextAfter: nextAfter,
	}))
}

type RepoCommit struct {
	Diff *git.NiceDiff
	Ref  string
	Desc string
}

func (h *handlers) commitHandler(w http.ResponseWriter, r *http.Request) {
	name := r.PathValue("name")
	ref := h.parseRef(r.PathValue("ref"))

	repo, err := h.openPublicRepo(name, ref)
	if err != nil {
		h.write404(w, r.URL.Path, err)
		return
	}

	diff, err := h.getDiff(repo, ref)
	if err != nil {
		h.write500(w, err)
		return
	}

	desc, err := repo.Description()
	if err != nil {
		h.write500(w, err)
		return
	}

	h.templ(w, "repo_commit", h.pageData(repo, RepoCommit{
		Desc: desc,
		Ref:  ref,
		Diff: diff,
	}))
}

type RepoCompare struct {
	Desc    string
	Ref     string
	Compare *git.Compare
}

func (h *handlers) compareHandler(w http.ResponseWriter, r *http.Request) {
	name := r.PathValue("name")
	ref1 := h.parseRef(r.PathValue("ref1"))
	ref2 := h.parseRef(r.PathValue("ref2"))

	repo, err := h.openPublicRepo(name, ref2)
	if err != nil {
		h.write404(w, r.URL.Path, err)
		return
	}

	desc, err := repo.Description()
	if err != nil {
		h.write500(w, err)
		return
	}

	compare, err := repo.Compare(ref1, ref2)
	if err != nil {
		h.write404(w, r.URL.Path, err)
		return
	}

	h.templ(w, "repo_compare", h.pageData(repo, RepoCompare{
		Desc:    desc,
		Ref:     ref2,
		Compare: compare,
	}))
}

type RepoRefs struct {
	Desc     string
	Ref      string
	Branches []*git.Branch
	Tags     []*git.TagReference
}

func (h *handlers) refsHandler(w http.ResponseWriter, r *http.Request) {
	repo, err := h.openPublicRepo(r.PathValue("name"), "")
	if err != nil {
		h.write404(w, r.URL.Path, err)
		return
	}

	desc, err := repo.Description()
	if err != nil {
		h.write500(w, err)
		return
	}

	master, err := repo.DefaultBranch()
	if err != nil {
		h.write500(w, err)
		return
	}

	branches, err := repo.Branches()
	if err != nil {
		h.write500(w, err)
		return
	}

	// repo should have at least one branch, tags are *optional*
	tags, _ := repo.Tags()

	h.templ(w, "repo_refs", h.pageData(repo, RepoRefs{
		Desc:     desc,
		Ref:      master,
		Tags:     tags,
		Branches: branches,
	}))
}

type repoList struct {
	Name       string
	Desc       string
	LastCommit time.Time
}

func (h *handlers) listPublicRepos() ([]repoList, error) {
	if v, found := h.repoListCache.Get("repo_list"); found {
		return v, nil
	}

	dirs, err := os.ReadDir(h.c.Repo.Dir)
	if err != nil {
		return nil, err
	}

	var repos []repoList
	var errs []error
	for _, dir := range dirs {
		if !dir.IsDir() {
			continue
		}

		name := dir.Name()
		repo, err := h.openPublicRepo(name, "")
		if err != nil {
			// if it's not git repo, just ignore it
			continue
		}

		desc, err := repo.Description()
		if err != nil {
			errs = append(errs, err)
			continue
		}

		lastCommit, err := repo.LastCommit()
		if err != nil {
			errs = append(errs, err)
			continue
		}

		repos = append(repos, repoList{
			Name:       repo.Name(),
			Desc:       desc,
			LastCommit: lastCommit.Committed,
		})
	}

	sort.Slice(repos, func(i, j int) bool {
		return repos[j].LastCommit.Before(repos[i].LastCommit)
	})

	h.repoListCache.Set("repo_list", repos)
	return repos, errors.Join(errs...)
}

func (h handlers) getDiff(r *git.Repo, ref string) (*git.NiceDiff, error) {
	cacheKey := fmt.Sprintf("%s:%s", r.Name(), ref)
	if v, found := h.diffCache.Get(cacheKey); found {
		return v, nil
	}

	diff, err := r.Diff()
	if err != nil {
		return nil, err
	}

	h.diffCache.Set(cacheKey, diff)
	return diff, nil
}

func (h *handlers) renderReadme(r *git.Repo, ref, treePath string) (template.HTML, error) {
	name := r.Name()
	cacheKey := fmt.Sprintf("%s:%s:%s", name, ref, treePath)
	if v, found := h.readmeCache.Get(cacheKey); found {
		return v, nil
	}

	var readmeContents template.HTML
	for _, readme := range h.c.Repo.Readmes {
		fullPath := filepath.Join(treePath, readme)
		fc, ferr := r.FileContent(fullPath)
		if ferr != nil {
			continue
		}

		if fc.IsBinary && fc.IsImage {
			continue
		}

		ext := filepath.Ext(readme)
		content := fc.String()
		if len(content) > 0 {
			switch ext {
			case ".md", ".markdown", ".mkd":
				readme, err := markdown.Render(name, ref, fullPath, content)
				if err != nil {
					return "", err
				}
				return template.HTML(readme), nil

			default:
				readmeContents = template.HTML(fmt.Sprintf(
					`<pre class="raw">%s</pre>`, html.EscapeString(content),
				))
			}
			break
		}
	}

	h.readmeCache.Set(cacheKey, readmeContents)
	return readmeContents, nil
}

func (h handlers) pageData[T any](repo *git.Repo, p T) PageData[T] {
	var name string
	var gomod, empty bool
	if repo != nil {
		gomod = repo.IsGoMod()
		empty = repo.IsEmpty()
		name = repo.Name()
	}

	return PageData[T]{
		P:        p,
		RepoName: name,
		Meta: Meta{
			Title:       h.c.Meta.Title,
			Description: h.c.Meta.Description,
			Host:        h.c.Meta.Host,
			GoMod:       gomod,
			SSHEnabled:  h.c.SSH.Enable,
			IsEmpty:     empty,
		},
	}
}

type Breadcrumb struct {
	Name   string
	Path   string
	IsLast bool
}

func Breadcrumbs(path string) []Breadcrumb {
	if path == "" {
		return nil
	}
	parts := strings.Split(path, "/")
	crumbs := make([]Breadcrumb, len(parts))
	for i, part := range parts {
		crumbs[i] = Breadcrumb{
			Name:   part,
			Path:   strings.Join(parts[:i+1], "/"),
			IsLast: i == len(parts)-1,
		}
	}
	return crumbs
}
