/*
Copyright © 2023 Philipp Wolfer <phw@uploadedlobster.com>

Scotty is free software: you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
Foundation, either version 3 of the License, or (at your option) any later version.

Scotty is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU General Public License for more details.

You should have received a copy of the GNU General Public License along with
Scotty. If not, see <https://www.gnu.org/licenses/>.
*/

package cli

import (
	"fmt"
	"math"
	"strconv"
	"sync"
	"time"

	"github.com/spf13/cobra"
	"github.com/spf13/viper"
	"go.uploadedlobster.com/scotty/internal/backends"
	"go.uploadedlobster.com/scotty/internal/config"
	"go.uploadedlobster.com/scotty/internal/i18n"
	"go.uploadedlobster.com/scotty/internal/models"
	"go.uploadedlobster.com/scotty/internal/storage"
)

func NewTransferCmd[
	E models.Backend,
	I models.ImportBackend,
	R models.ListensResult | models.LovesResult,
](
	cmd *cobra.Command,
	db *storage.Database,
	entity string,
	source string,
	target string,
) (TransferCmd[E, I, R], error) {
	c := TransferCmd[E, I, R]{
		cmd:    cmd,
		db:     db,
		entity: entity,
	}
	err := c.resolveBackends(source, target)
	if err != nil {
		return c, err
	}
	return c, nil
}

type TransferCmd[E models.Backend, I models.ImportBackend, R models.ListensResult | models.LovesResult] struct {
	cmd        *cobra.Command
	db         *storage.Database
	entity     string
	sourceName string
	targetName string
	ExpBackend E
	ImpBackend I
}

func (c *TransferCmd[E, I, R]) resolveBackends(source string, target string) error {
	sourceConfig, err := config.GetService(source)
	cobra.CheckErr(err)
	targetConfig, err := config.GetService(target)
	cobra.CheckErr(err)

	// Initialize backends
	expBackend, err := backends.ResolveBackend[E](sourceConfig)
	if err != nil {
		return err
	}
	impBackend, err := backends.ResolveBackend[I](targetConfig)
	if err != nil {
		return err
	}

	c.sourceName = sourceConfig.Name
	c.targetName = targetConfig.Name
	c.ExpBackend = expBackend
	c.ImpBackend = impBackend
	return nil
}

func (c *TransferCmd[E, I, R]) Transfer(exp backends.ExportProcessor[R], imp backends.ImportProcessor[R]) error {
	fmt.Println(i18n.Tr("Transferring %s from %s to %s...", c.entity, c.sourceName, c.targetName))

	// Authenticate backends, if needed
	config := viper.GetViper()
	_, err := backends.Authenticate(c.sourceName, c.ExpBackend, *c.db, config)
	if err != nil {
		return err
	}

	_, err = backends.Authenticate(c.targetName, c.ImpBackend, *c.db, config)
	if err != nil {
		return err
	}

	// Read timestamp
	timestamp, err := c.timestamp()
	if err != nil {
		return err
	}
	printTimestamp("From timestamp: %v (%v)", timestamp)

	// Prepare progress bars
	exportProgress := make(chan models.Progress)
	importProgress := make(chan models.Progress)
	var wg sync.WaitGroup
	progress := progressBar(&wg, exportProgress, importProgress)

	// Export from source
	exportChan := make(chan R, 1000)
	go exp.Process(timestamp, exportChan, exportProgress)

	// Import into target
	resultChan := make(chan models.ImportResult)
	go imp.Process(exportChan, resultChan, importProgress)
	result := <-resultChan
	if result.LastTimestamp.Unix() < timestamp.Unix() {
		result.LastTimestamp = timestamp
	}
	close(exportProgress)
	wg.Wait()
	progress.Wait()
	if result.Error != nil {
		printTimestamp("Import failed, last reported timestamp was %v (%s)", result.LastTimestamp)
		return result.Error
	}
	fmt.Println(i18n.Tr("Imported %v of %v %s into %v.",
		result.ImportCount, result.TotalCount, c.entity, c.targetName))

	// Update timestamp
	err = c.updateTimestamp(result, timestamp)
	if err != nil {
		return err
	}

	// Print errors
	if len(result.ImportErrors) > 0 {
		fmt.Println()
		fmt.Println(i18n.Tr("During the import the following errors occurred:"))
		for _, err := range result.ImportErrors {
			fmt.Println(i18n.Tr("Error: %v\n", err))
		}
	}

	return nil
}

func (c *TransferCmd[E, I, R]) timestamp() (time.Time, error) {
	flagValue, err := c.cmd.Flags().GetInt64("timestamp")
	if err == nil && flagValue > math.MinInt64 {
		return time.Unix(flagValue, 0), nil
	} else {
		timestamp, err := c.db.GetImportTimestamp(c.sourceName, c.targetName, c.entity)
		return timestamp, err
	}
}

func (c *TransferCmd[E, I, R]) updateTimestamp(result models.ImportResult, oldTimestamp time.Time) error {
	if result.LastTimestamp.Unix() < oldTimestamp.Unix() {
		result.LastTimestamp = oldTimestamp
	}
	printTimestamp("Latest timestamp: %v (%v)", result.LastTimestamp)
	err := c.db.SetImportTimestamp(c.sourceName, c.targetName, c.entity, result.LastTimestamp)
	return err
}

func printTimestamp(s string, t time.Time) {
	fmt.Println(i18n.Tr(s, t, strconv.FormatInt(t.Unix(), 10)))
}