package backend

import (
	"context"
	"fmt"
	"io"
	"os"
	"path/filepath"
	"strings"
	"sync"

	"github.com/charmbracelet/log/v2"
	"github.com/charmbracelet/soft-serve/git"
	"github.com/charmbracelet/soft-serve/pkg/config"
	"github.com/charmbracelet/soft-serve/pkg/hooks"
	"github.com/charmbracelet/soft-serve/pkg/proto"
	"github.com/charmbracelet/soft-serve/pkg/sshutils"
	"github.com/charmbracelet/soft-serve/pkg/webhook"
	bugCache "github.com/git-bug/git-bug/cache"
	"github.com/git-bug/git-bug/repository"
)

var _ hooks.Hooks = (*Backend)(nil)

// PostReceive is called by the git post-receive hook.
//
// It implements Hooks.
func (d *Backend) PostReceive(ctx context.Context, _ io.Writer, _ io.Writer, repo string, args []hooks.HookArg) {
	d.logger.Debug("post-receive hook called", "repo", repo, "args", args)

	hasGitBugRefs := false
	for _, arg := range args {
		if isGitBugRef(arg.RefName) {
			hasGitBugRefs = true
			d.logger.Info("git-bug ref detected, rebuilding cache", "repo", repo, "ref", arg.RefName)
			if err := rebuildGitBugCache(d.cfg, repo, d.logger); err != nil {
				d.logger.Error("failed to rebuild git-bug cache", "repo", repo, "err", err)
			}
			break
		}
	}
	
	if !hasGitBugRefs {
		d.logger.Debug("no git-bug refs detected in push", "repo", repo)
	}
}

// PreReceive is called by the git pre-receive hook.
//
// It implements Hooks.
func (d *Backend) PreReceive(_ context.Context, _ io.Writer, _ io.Writer, repo string, args []hooks.HookArg) {
	d.logger.Debug("pre-receive hook called", "repo", repo, "args", args)
}

// Update is called by the git update hook.
//
// It implements Hooks.
func (d *Backend) Update(ctx context.Context, _ io.Writer, _ io.Writer, repo string, arg hooks.HookArg) {
	d.logger.Debug("update hook called", "repo", repo, "arg", arg)

	// Find user
	var user proto.User
	if pubkey := os.Getenv("SOFT_SERVE_PUBLIC_KEY"); pubkey != "" {
		pk, _, err := sshutils.ParseAuthorizedKey(pubkey)
		if err != nil {
			d.logger.Error("error parsing public key", "err", err)
			return
		}

		user, err = d.UserByPublicKey(ctx, pk)
		if err != nil {
			d.logger.Error("error finding user from public key", "key", pubkey, "err", err)
			return
		}
	} else if username := os.Getenv("SOFT_SERVE_USERNAME"); username != "" {
		var err error
		user, err = d.User(ctx, username)
		if err != nil {
			d.logger.Error("error finding user from username", "username", username, "err", err)
			return
		}
	} else {
		d.logger.Error("error finding user")
		return
	}

	// Get repo
	r, err := d.Repository(ctx, repo)
	if err != nil {
		d.logger.Error("error finding repository", "repo", repo, "err", err)
		return
	}

	// TODO: run this async
	// This would probably need something like an RPC server to communicate with the hook process.
	if git.IsZeroHash(arg.OldSha) || git.IsZeroHash(arg.NewSha) {
		wh, err := webhook.NewBranchTagEvent(ctx, user, r, arg.RefName, arg.OldSha, arg.NewSha)
		if err != nil {
			d.logger.Error("error creating branch_tag webhook", "err", err)
		} else if err := webhook.SendEvent(ctx, wh); err != nil {
			d.logger.Error("error sending branch_tag webhook", "err", err)
		}
	}
	wh, err := webhook.NewPushEvent(ctx, user, r, arg.RefName, arg.OldSha, arg.NewSha)
	if err != nil {
		d.logger.Error("error creating push webhook", "err", err)
	} else if err := webhook.SendEvent(ctx, wh); err != nil {
		d.logger.Error("error sending push webhook", "err", err)
	}
}

// PostUpdate is called by the git post-update hook.
//
// It implements Hooks.
func (d *Backend) PostUpdate(ctx context.Context, _ io.Writer, _ io.Writer, repo string, args ...string) {
	d.logger.Debug("post-update hook called", "repo", repo, "args", args)

	var wg sync.WaitGroup

	// Populate last-modified file.
	wg.Add(1)
	go func() {
		defer wg.Done()
		if err := populateLastModified(ctx, d, repo); err != nil {
			d.logger.Error("error populating last-modified", "repo", repo, "err", err)
			return
		}
	}()

	wg.Wait()
}

func populateLastModified(ctx context.Context, d *Backend, name string) error {
	var rr *repo
	_rr, err := d.Repository(ctx, name)
	if err != nil {
		return err
	}

	if r, ok := _rr.(*repo); ok {
		rr = r
	} else {
		return proto.ErrRepoNotFound
	}

	r, err := rr.Open()
	if err != nil {
		return err
	}

	c, err := r.LatestCommitTime()
	if err != nil {
		return err
	}

	return rr.writeLastModified(c)
}

func isGitBugRef(refName string) bool {
	return strings.HasPrefix(refName, "refs/bugs/") || strings.HasPrefix(refName, "refs/identities/")
}

func rebuildGitBugCache(cfg *config.Config, repoName string, logger *log.Logger) error {
	logger.Info("starting git-bug cache rebuild", "repo", repoName)
	repoPath := filepath.Join(cfg.DataPath, "repos", repoName+".git")

	goGitRepo, err := repository.OpenGoGitRepo(repoPath, "git-bug", nil)
	if err != nil {
		return fmt.Errorf("open go-git repo: %w", err)
	}
	defer goGitRepo.Close()

	rc, err := bugCache.NewRepoCacheNoEvents(goGitRepo)
	if err != nil {
		return fmt.Errorf("create repo cache: %w", err)
	}
	defer func() {
		if closeErr := rc.Close(); closeErr != nil {
			logger.Error("failed to close git-bug cache", "repo", repoName, "err", closeErr)
		}
	}()

	var identityTotal int64
	logger.Debug("rebuilding identities cache", "repo", repoName)
	for ev := range rc.Identities().Build() {
		if ev.Err != nil {
			return fmt.Errorf("rebuild identities cache: %w", ev.Err)
		}
		logger.Debug("identity build event",
			"repo", repoName,
			"event", ev.Event,
			"typename", ev.Typename,
			"total", ev.Total,
			"progress", ev.Progress)
		
		if ev.Total > 0 {
			identityTotal = ev.Total
		}
	}
	logger.Info("identities cache rebuilt", "repo", repoName, "count", identityTotal)

	var bugTotal int64
	logger.Debug("rebuilding bugs cache", "repo", repoName)
	for ev := range rc.Bugs().Build() {
		if ev.Err != nil {
			return fmt.Errorf("rebuild bugs cache: %w", ev.Err)
		}
		logger.Debug("bug build event",
			"repo", repoName,
			"event", ev.Event,
			"typename", ev.Typename,
			"total", ev.Total,
			"progress", ev.Progress)
		
		if ev.Total > 0 {
			bugTotal = ev.Total
		}
	}
	logger.Info("bugs cache rebuilt", "repo", repoName, "count", bugTotal)

	logger.Info("git-bug cache rebuild complete", "repo", repoName, "identities", identityTotal, "bugs", bugTotal)
	return nil
}
