package web

import (
	"context"
	"fmt"
	"html/template"
	"net/http"
	"os"
	"path/filepath"
	"sort"
	"strconv"
	"time"

	"github.com/charmbracelet/log/v2"
	"github.com/charmbracelet/soft-serve/pkg/config"
	"github.com/charmbracelet/soft-serve/pkg/proto"
	"github.com/git-bug/git-bug/cache"
	"github.com/git-bug/git-bug/entities/bug"
	"github.com/git-bug/git-bug/entities/common"
	"github.com/git-bug/git-bug/repository"
	"github.com/gorilla/mux"
)

const defaultBugsPerPage = 20

type BugsData struct {
	RepoBaseData
	PaginationData

	Bugs   []BugListItem
	Status string
}

type BugListItem struct {
	ID           string
	FullID       string
	Title        string
	Author       string
	AuthorAvatar string
	Status       string
	CreatedAt    time.Time
	LastActivity time.Time
	HasActivity  bool
	CommentCount int
	Labels       []Label
}

type BugData struct {
	RepoBaseData

	ID        string
	Subject   string
	Status    string
	Author    string
	CreatedAt time.Time
	Edited    bool
	Timeline  []TimelineItem
	Labels    []Label
}

type TimelineItem struct {
	Type         string
	ID           string
	Author       string
	AuthorAvatar string
	Timestamp    time.Time
	Edited       bool

	Message       template.HTML
	Title         string
	Status        string
	AddedLabels   []Label
	RemovedLabels []Label
}

type Label struct {
	Name  string
	Color string
}

func hasGitBug(ctx context.Context, repo proto.Repository) bool {
	cfg := config.FromContext(ctx)
	repoPath := filepath.Join(cfg.DataPath, "repos", repo.Name()+".git")
	bugsPath := filepath.Join(repoPath, "refs", "bugs")

	info, err := os.Stat(bugsPath)
	if err != nil {
		return false
	}
	return info.IsDir()
}

func openBugCache(ctx context.Context, repo proto.Repository) (*cache.RepoCache, error) {
	cfg := config.FromContext(ctx)
	repoPath := filepath.Join(cfg.DataPath, "repos", repo.Name()+".git")

	goGitRepo, err := repository.OpenGoGitRepo(repoPath, "git-bug", nil)
	if err != nil {
		return nil, err
	}

	rc, err := cache.NewRepoCacheNoEvents(goGitRepo)
	if err != nil {
		goGitRepo.Close()
		return nil, err
	}

	return rc, nil
}

func getBugsList(rc *cache.RepoCache, status string) ([]BugListItem, error) {
	allBugs := rc.Bugs().AllIds()
	bugs := make([]BugListItem, 0)

	for _, id := range allBugs {
		bugCache, err := rc.Bugs().Resolve(id)
		if err != nil {
			continue
		}

		snap := bugCache.Snapshot()

		if status != "all" && snap.Status.String() != status {
			continue
		}

		labels := make([]Label, len(snap.Labels))
		for i, label := range snap.Labels {
			labels[i] = labelToWebLabel(label)
		}

		bugs = append(bugs, BugListItem{
			ID:           snap.Id().Human(),
			FullID:       snap.Id().String(),
			Title:        snap.Title,
			Author:       snap.Author.DisplayName(),
			AuthorAvatar: snap.Author.AvatarUrl(),
			Status:       snap.Status.String(),
			CreatedAt:    snap.CreateTime,
			LastActivity: getLastActivity(snap),
			HasActivity:  len(snap.Timeline) > 1,
			CommentCount: countComments(snap),
			Labels:       labels,
		})
	}

	sort.Slice(bugs, func(i, j int) bool {
		return bugs[i].LastActivity.After(bugs[j].LastActivity)
	})

	return bugs, nil
}

func getLastActivity(snap *bug.Snapshot) time.Time {
	var lastTime time.Time

	for _, item := range snap.Timeline {
		var itemTime time.Time

		switch op := item.(type) {
		case *bug.CreateTimelineItem:
			itemTime = op.CreatedAt.Time()
		case *bug.AddCommentTimelineItem:
			itemTime = op.CreatedAt.Time()
		case *bug.SetTitleTimelineItem:
			itemTime = op.UnixTime.Time()
		case *bug.SetStatusTimelineItem:
			itemTime = op.UnixTime.Time()
		case *bug.LabelChangeTimelineItem:
			itemTime = op.UnixTime.Time()
		}

		if itemTime.After(lastTime) {
			lastTime = itemTime
		}
	}

	return lastTime
}

func countComments(snap *bug.Snapshot) int {
	count := 0
	for _, item := range snap.Timeline {
		if _, ok := item.(*bug.AddCommentTimelineItem); ok {
			count++
		}
	}
	return count
}

func buildTimelineItems(snap *bug.Snapshot) []TimelineItem {
	items := make([]TimelineItem, 0, len(snap.Timeline))

	for _, item := range snap.Timeline {
		switch op := item.(type) {
		case *bug.CreateTimelineItem:
			messageHTML := template.HTML("")
			if !op.MessageIsEmpty() {
				rendered, err := renderMarkdown([]byte(op.Message))
				if err != nil {
					messageHTML = template.HTML(template.HTMLEscapeString(op.Message))
				} else {
					messageHTML = rendered
				}
			}

			items = append(items, TimelineItem{
				Type:         "create",
				ID:           op.CombinedId().String(),
				Author:       op.Author.DisplayName(),
				AuthorAvatar: op.Author.AvatarUrl(),
				Timestamp:    op.CreatedAt.Time(),
				Edited:       op.Edited(),
				Message:      messageHTML,
			})

		case *bug.AddCommentTimelineItem:
			messageHTML := template.HTML("")
			if !op.MessageIsEmpty() {
				rendered, err := renderMarkdown([]byte(op.Message))
				if err != nil {
					messageHTML = template.HTML(template.HTMLEscapeString(op.Message))
				} else {
					messageHTML = rendered
				}
			}

			items = append(items, TimelineItem{
				Type:         "comment",
				ID:           op.CombinedId().String(),
				Author:       op.Author.DisplayName(),
				AuthorAvatar: op.Author.AvatarUrl(),
				Timestamp:    op.CreatedAt.Time(),
				Edited:       op.Edited(),
				Message:      messageHTML,
			})

		case *bug.SetTitleTimelineItem:
			items = append(items, TimelineItem{
				Type:         "title",
				ID:           op.CombinedId().String(),
				Author:       op.Author.DisplayName(),
				AuthorAvatar: op.Author.AvatarUrl(),
				Timestamp:    op.UnixTime.Time(),
				Title:        op.Title,
			})

		case *bug.SetStatusTimelineItem:
			items = append(items, TimelineItem{
				Type:         "status",
				ID:           op.CombinedId().String(),
				Author:       op.Author.DisplayName(),
				AuthorAvatar: op.Author.AvatarUrl(),
				Timestamp:    op.UnixTime.Time(),
				Status:       op.Status.Action(),
			})

		case *bug.LabelChangeTimelineItem:
			added := make([]Label, len(op.Added))
			for i, label := range op.Added {
				added[i] = labelToWebLabel(label)
			}

			removed := make([]Label, len(op.Removed))
			for i, label := range op.Removed {
				removed[i] = labelToWebLabel(label)
			}

			items = append(items, TimelineItem{
				Type:          "labels",
				ID:            op.CombinedId().String(),
				Author:        op.Author.DisplayName(),
				AuthorAvatar:  op.Author.AvatarUrl(),
				Timestamp:     op.UnixTime.Time(),
				AddedLabels:   added,
				RemovedLabels: removed,
			})
		}
	}

	return items
}

func findBugByHash(rc *cache.RepoCache, hash string) (*cache.BugCache, error) {
	allBugs := rc.Bugs().AllIds()
	for _, id := range allBugs {
		if id.String() == hash || id.Human() == hash {
			return rc.Bugs().Resolve(id)
		}
	}
	return nil, fmt.Errorf("bug not found")
}

func labelToWebLabel(label common.Label) Label {
	rgba := label.Color().RGBA()
	return Label{
		Name:  label.String(),
		Color: fmt.Sprintf("#%02x%02x%02x", rgba.R, rgba.G, rgba.B),
	}
}

func repoBugs(w http.ResponseWriter, r *http.Request) {
	ctx := r.Context()
	logger := log.FromContext(ctx)
	cfg := config.FromContext(ctx)
	repo := proto.RepositoryFromContext(ctx)

	if !hasGitBug(ctx, repo) {
		renderNotFound(w, r)
		return
	}

	rc, err := openBugCache(ctx, repo)
	if err != nil {
		logger.Debug("failed to open bug cache", "repo", repo.Name(), "err", err)
		renderNotFound(w, r)
		return
	}
	defer func() {
		if closeErr := rc.Close(); closeErr != nil {
			logger.Debug("failed to close bug cache", "repo", repo.Name(), "err", closeErr)
		}
	}()

	status := r.URL.Query().Get("status")
	if status == "" {
		status = "open"
	}
	if status != "all" && status != "open" && status != "closed" {
		status = "open"
	}

	page := 1
	if pageStr := r.URL.Query().Get("page"); pageStr != "" {
		if p, err := strconv.Atoi(pageStr); err == nil && p > 0 {
			page = p
		}
	}

	bugs, err := getBugsList(rc, status)
	if err != nil {
		logger.Debug("failed to get bugs list", "repo", repo.Name(), "err", err)
		renderInternalServerError(w, r)
		return
	}

	totalBugs := len(bugs)
	totalPages := (totalBugs + defaultBugsPerPage - 1) / defaultBugsPerPage
	if totalPages < 1 {
		totalPages = 1
	}
	if page > totalPages && totalPages > 0 {
		page = totalPages
	}
	if page < 1 {
		page = 1
	}

	start := (page - 1) * defaultBugsPerPage
	end := start + defaultBugsPerPage
	if end > totalBugs {
		end = totalBugs
	}

	pagedBugs := bugs[start:end]

	gr, err := openRepository(repo)
	if err != nil {
		logger.Debug("failed to open repository", "repo", repo.Name(), "err", err)
		renderInternalServerError(w, r)
		return
	}
	defaultBranch := getDefaultBranch(gr)

	repoDisplayName := repo.ProjectName()
	if repoDisplayName == "" {
		repoDisplayName = repo.Name()
	}

	data := BugsData{
		RepoBaseData: RepoBaseData{
			BaseData: BaseData{
				ServerName: cfg.Name,
				ActiveTab:  "bugs",
				Title:      "Bugs | " + repoDisplayName,
			},
			Repo:          repo,
			DefaultBranch: defaultBranch,
			HasGitBug:     true,
		},
		PaginationData: PaginationData{
			Page:        page,
			TotalPages:  totalPages,
			HasPrevPage: page > 1,
			HasNextPage: page < totalPages,
		},
		Bugs:   pagedBugs,
		Status: status,
	}

	renderHTML(w, "bugs.html", data)
}

func repoBug(w http.ResponseWriter, r *http.Request) {
	ctx := r.Context()
	logger := log.FromContext(ctx)
	cfg := config.FromContext(ctx)
	repo := proto.RepositoryFromContext(ctx)
	vars := mux.Vars(r)
	hash := vars["hash"]

	if !hasGitBug(ctx, repo) {
		renderNotFound(w, r)
		return
	}

	rc, err := openBugCache(ctx, repo)
	if err != nil {
		logger.Debug("failed to open bug cache", "repo", repo.Name(), "err", err)
		renderNotFound(w, r)
		return
	}
	defer func() {
		if closeErr := rc.Close(); closeErr != nil {
			logger.Debug("failed to close bug cache", "repo", repo.Name(), "err", closeErr)
		}
	}()

	bugCache, err := findBugByHash(rc, hash)
	if err != nil {
		logger.Debug("failed to find bug", "repo", repo.Name(), "hash", hash, "err", err)
		renderNotFound(w, r)
		return
	}

	snap := bugCache.Snapshot()
	timeline := buildTimelineItems(snap)

	labels := make([]Label, len(snap.Labels))
	for i, label := range snap.Labels {
		labels[i] = labelToWebLabel(label)
	}

	var edited bool
	if len(timeline) > 0 && timeline[0].Type == "create" {
		edited = timeline[0].Edited
	}

	gr, err := openRepository(repo)
	if err != nil {
		logger.Debug("failed to open repository", "repo", repo.Name(), "err", err)
		renderInternalServerError(w, r)
		return
	}
	defaultBranch := getDefaultBranch(gr)

	repoDisplayName := repo.ProjectName()
	if repoDisplayName == "" {
		repoDisplayName = repo.Name()
	}

	data := BugData{
		RepoBaseData: RepoBaseData{
			BaseData: BaseData{
				ServerName: cfg.Name,
				ActiveTab:  "bugs",
				Title:      snap.Title + " | Bug " + snap.Id().Human() + " | " + repoDisplayName,
			},
			Repo:          repo,
			DefaultBranch: defaultBranch,
			HasGitBug:     true,
		},
		ID:        snap.Id().Human(),
		Subject:   snap.Title,
		Status:    snap.Status.String(),
		Author:    snap.Author.DisplayName(),
		CreatedAt: snap.CreateTime,
		Edited:    edited,
		Timeline:  timeline,
		Labels:    labels,
	}

	renderHTML(w, "bug.html", data)
}
