package domain

import (
	"context"
	"fmt"
	"io"
	"strings"
	"time"

	catalog "git.vdhsn.com/barretthousen/barretthousen/src/catalog/api"
	"git.vdhsn.com/barretthousen/barretthousen/src/lib/kernel"
	"git.vdhsn.com/barretthousen/barretthousen/src/runner/internal/data/kafka"
	"golang.org/x/sync/errgroup"
)

var targetsImpls = map[string]UpcomingAuctionFinder{}

type UpcomingAuctionFinder interface {
	fmt.Stringer
	Find(ctx context.Context, limit int, results chan<- catalog.Auction) error
}

func RegisterAuctionFinder(finder UpcomingAuctionFinder) {
	targetName := finder.String()
	if _, ok := targetsImpls[targetName]; ok {
		kernel.FatalErr(fmt.Errorf("target %q has already been registered", targetName))
	}

	targetsImpls[targetName] = finder
}

type (
	Domain struct {
		Storage
	}

	CompleteScrapeJobStatus struct {
		AuctionCount int
		Errors       string
	}

	Storage interface {
		CreateScrapeJob(context.Context, string) (ScrapeJob, error)
		CompleteScrapeJob(context.Context, int, CompleteScrapeJobStatus) (ScrapeJob, error)
		GetJobs(context.Context) ([]ScrapeJob, error)
	}

	KafkaProducer interface {
		io.Closer
		SendMessageJSON(map[string]string, interface{}) error
	}

	FindNewUpcomingInput struct {
		TargetSite string
	}

	FindNewUpcomingOutput struct {
		Job ScrapeJob
	}
)

func (domain Domain) StartSync(ctx context.Context, in FindNewUpcomingInput) (out FindNewUpcomingOutput, err error) {
	kernel.TraceLog.Printf("%+v", in)

	runAllFinders := in.TargetSite == "" || in.TargetSite == "all"
	for targetSite, finder := range targetsImpls {
		if !runAllFinders && in.TargetSite != targetSite {
			continue
		}

		if out.Job, err = domain.Storage.CreateScrapeJob(ctx, targetSite); err != nil {
			err = fmt.Errorf("could not create new scrape job record: %w", err)
			return
		}

		kernel.InfoLog.Printf("Scrape Job %d starting", out.Job.ID)

		// TODO: make everything after this line async and run after return
		go domain.executeScrapeJob(finder, out.Job.ID)
	}

	return
}

type (
	GetJobsInput  struct{}
	GetJobsOutput struct {
		Jobs []ScrapeJob
	}
)

func (domain Domain) Status(ctx context.Context, in GetJobsInput) (out GetJobsOutput, err error) {
	scrapeJobs, err := domain.Storage.GetJobs(ctx)
	if err != nil {
		err = fmt.Errorf("could not fetch jobs from storage: %w", err)
		return
	}

	out = GetJobsOutput{Jobs: scrapeJobs}

	return
}

// TODO: tests
func (domain *Domain) executeScrapeJob(finder UpcomingAuctionFinder, jobID int) {
	ctx, cancel := context.WithCancel(context.TODO())
	defer cancel()
	found := make(chan catalog.Auction, 2048)

	deadlineCtx, deadlineCancel := context.WithTimeout(ctx, time.Minute*5)
	defer deadlineCancel()
	errGroup, innerCtx := errgroup.WithContext(deadlineCtx)

	errGroup.Go(func() error {
		return finder.Find(innerCtx, 0, found)
	})

	var p KafkaProducer
	var err error
	if p, err = kafka.NewProducer("runner.sync_results"); err != nil {
		return
	}
	defer p.Close()

	count := 0
	total := 0
	errs := &strings.Builder{}
	for auction := range found {
		total++
		if !auction.Start.After(time.Now()) {
			continue
		}

		if err := p.SendMessageJSON(map[string]string{
			"scrape-job-id": fmt.Sprintf("%d", jobID),
			"target-site":   finder.String(),
		}, auction); err != nil {
			kernel.TraceLog.Printf("could not publish auction to kafka: %v", err)
			continue
		}

		count++
	}

	if err := errGroup.Wait(); err != nil {
		err = fmt.Errorf("an issue occurred while finding upcoming items iteration: %w", err)
		fmt.Fprintf(errs, "{ \"error\": \"%s\" }", err.Error())
	}

	var completedJob ScrapeJob
	if completedJob, err = domain.Storage.CompleteScrapeJob(ctx, jobID, CompleteScrapeJobStatus{
		AuctionCount: count,
		Errors:       errs.String(),
	}); err != nil {
		kernel.ErrorLog.Printf("Could not complete scrape job, failing: %v", err)
	}

	kernel.InfoLog.Printf("Scrape Job %d completed in %v. Successfully imported %d/%d", jobID, completedJob.Completed.Sub(completedJob.Started), count, total)
	return
}
