package mirror

import (
	"context"
	"errors"
	"fmt"
	"log/slog"
	"os"
	"path/filepath"
	"strings"
	"sync"
	"time"

	"golang.org/x/sync/semaphore"

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

func IsRemoteSupported(remote string) error {
	if !strings.HasPrefix(remote, "http") {
		return fmt.Errorf("only http and https remotes are supported")
	}
	return nil
}

func IsGithubRemote(remoteURL string) bool {
	return strings.Contains(remoteURL, "github.com")
}

type Worker struct {
	c *config.Config
}

func NewWorker(cfg *config.Config) *Worker {
	return &Worker{
		c: cfg,
	}
}

func (w *Worker) Start(ctx context.Context) error {
	ticker := time.NewTicker(w.c.Mirror.Interval)
	defer ticker.Stop()

	for {
		select {
		case <-ctx.Done():
			return nil
		default:
			if err := w.mirror(ctx); err != nil {
				slog.Error("mirror sync failed", "err", err)
			}

			<-ticker.C
		}
	}
}

func (w *Worker) SyncRepo(ctx context.Context, name string) error {
	path, err := git.ResolvePath(w.c.Repo.Dir, git.ResolveName(name))
	if err != nil {
		return fmt.Errorf("failed to resolve repo path: %w", err)
	}

	repo, err := git.Open(path, "")
	if err != nil {
		return fmt.Errorf("failed to open repo: %w", err)
	}

	isMirror, err := repo.IsMirror()
	if err != nil {
		return fmt.Errorf("failed to check mirror status: %w", err)
	}
	if !isMirror {
		return fmt.Errorf("repository is not a mirror")
	}

	return w.syncRepo(ctx, repo)
}

func (w *Worker) mirror(ctx context.Context) error {
	repos, err := w.findMirrorRepos()
	if err != nil {
		return err
	}

	var wg sync.WaitGroup
	sem := semaphore.NewWeighted(10)
	errCh := make(chan error, len(repos))

	for _, repo := range repos {
		wg.Go(func() {
			if err := sem.Acquire(ctx, 1); err != nil {
				errCh <- err
				return
			}
			defer sem.Release(1)

			if err := w.syncRepo(ctx, repo); err != nil {
				errCh <- err
			}
		})
	}

	wg.Wait()
	close(errCh)

	var errs []error
	for err := range errCh {
		errs = append(errs, err)
	}
	return errors.Join(errs...)
}

func (w *Worker) syncRepo(ctx context.Context, repo *git.Repo) error {
	name := repo.Name()
	slog.Info("mirror: sync started", "repo", name)

	remoteURL, err := repo.RemoteURL()
	if err != nil {
		slog.Error("mirror: failed to get remote url", "repo", name, "err", err)
		return err
	}

	if err = IsRemoteSupported(remoteURL); err != nil {
		slog.Error("mirror: remote is not valid", "repo", name, "err", err)
		return err
	}

	var isUpdated bool
	if IsGithubRemote(remoteURL) && w.c.Mirror.GithubToken != "" {
		isUpdated, err = repo.FetchFromGithubWithToken(ctx, w.c.Mirror.GithubToken)
	} else {
		isUpdated, err = repo.Fetch(ctx)
	}
	if err != nil {
		slog.Error("mirror: fetch failed", "repo", name, "err", err)
		return err
	}

	now := time.Now()
	if err := repo.SetLastChecked(now); err != nil {
		slog.Error("mirror: failed to set last checked time", "repo", name, "err", err)
	}

	if isUpdated {
		if err := repo.SetLastSync(now); err != nil {
			slog.Error("mirror: failed to set last sync time", "repo", name, "err", err)
		}
	}

	slog.Info("mirror: sync completed", "repo", repo.Name(), "updated", isUpdated)
	return nil
}

func (w *Worker) findMirrorRepos() ([]*git.Repo, error) {
	dirs, err := os.ReadDir(w.c.Repo.Dir)
	if err != nil {
		return nil, err
	}

	var repos []*git.Repo
	for _, dir := range dirs {
		if !dir.IsDir() {
			continue
		}

		name := dir.Name()
		path := filepath.Join(w.c.Repo.Dir, filepath.Clean(name))
		repo, err := git.Open(path, "")
		if err != nil {
			slog.Debug("skipping non-git directory", "name", name, "err", err)
			continue
		}

		isMirror, err := repo.IsMirror()
		if err != nil {
			slog.Debug("skipping non-mirror repo", "name", name, "err", err)
			continue
		}

		if isMirror {
			repos = append(repos, repo)
		}
	}

	return repos, nil
}
