package domain

import (
	"context"
	"errors"
	"fmt"
	"strings"
	"time"

	catalog "git.vdhsn.com/barretthousen/barretthousen/src/catalog/api"
	"git.vdhsn.com/barretthousen/barretthousen/src/lib/kernel"
	"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
		CatalogService
	}

	CompleteScrapeJobStatus struct {
		AuctionCount int
		Errors       string
	}

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

	CatalogService interface {
		UpdateUpcomingAuction(context.Context, catalog.Auction) (catalog.AuctionCreatedEvent, 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)

	finder, ok := targetsImpls[in.TargetSite]
	if !ok {
		kernel.TraceLog.Println("could not find target")
		err = errors.New("No scrape job found by name")
		return
	}

	if out.Job, err = domain.Storage.CreateScrapeJob(ctx, in.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 {
		Page  int32
		Limit int32
	}
	GetJobsResult struct {
		Total          int
		ActiveTotal    int
		Active         []ScrapeJob
		CompletedTotal int
		Complete       []ScrapeJob
	}
)

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

	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.WithDeadline(ctx, time.Now().Add(time.Minute))
	defer deadlineCancel()
	errGroup, innerCtx := errgroup.WithContext(deadlineCtx)

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

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

		ace, err := domain.CatalogService.UpdateUpcomingAuction(ctx, auction)
		if err != nil {
			kernel.ErrorLog.Printf("could not import upcoming auction: %s", err.Error())
			fmt.Fprintf(errs, "{ \"AuctionFingerprint\": \"%s\", \"error\": \"%s\" }\n", ace.Fingerprint, err.Error())
			continue
		}

		if !ace.Duplicate {
			count++
		}
	}

	kernel.TraceLog.Println("waiting for results...")
	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
	var err error
	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
}
