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 {
	Repo          proto.Repository
	DefaultBranch string
	ActiveTab     string
	ServerName    string
	HasGitBug     bool

	Bugs        []BugListItem
	Status      string
	Page        int
	TotalPages  int
	HasPrevPage bool
	HasNextPage bool
}

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

type BugData struct {
	Repo          proto.Repository
	DefaultBranch string
	ActiveTab     string
	ServerName    string
	HasGitBug     bool

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

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

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

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
		}

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

	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(),
				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(),
				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(),
				Timestamp: op.UnixTime.Time(),
				Title:     op.Title,
			})

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

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

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

			items = append(items, TimelineItem{
				Type:          "labels",
				ID:            op.CombinedId().String(),
				Author:        op.Author.DisplayName(),
				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()
	// RGBA() returns 16-bit channels (0-65535), convert to 8-bit (0-255)
	r8 := uint8(rgba.R >> 8)
	g8 := uint8(rgba.G >> 8)
	b8 := uint8(rgba.B >> 8)
	return Label{
		Name:  label.String(),
		Color: fmt.Sprintf("#%02x%02x%02x", r8, g8, b8),
	}
}

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)

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

	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)

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

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