package config

import (
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"net/http"
	"os"
	"path/filepath"
	"sort"

	"github.com/BurntSushi/toml"
	"golang.org/x/sync/errgroup"

	"github.com/make-go-great/copy-go"
	"github.com/make-go-great/diff-go"
)

const (
	configDirPath  = "data"
	configFileJSON = "data.json"
	configFileTOML = "data.toml"
)

var (
	ErrConfigNotFound = errors.New("config not found")
	ErrConfigInvalid  = errors.New("config invalid")
)

type Config interface {
	Install(appNames ...string) error
	Update(appNames ...string) error
	Clean() error
	Diff(appNames ...string) error
	Download(appNames ...string) error
	Validate(appNames ...string) error
	List() []string
}

type cfg struct {
	cfgApps  ConfigApps
	isDryRun bool
}

// LoadConfig return config, configDemo
func LoadConfig(path string, isDryRun bool) (Config, error) {
	configPathJSON := filepath.Join(path, configDirPath, configFileJSON)
	bytes, err := os.ReadFile(configPathJSON)
	if err == nil {
		return loadConfig(bytes, isDryRun, json.Unmarshal)
	}

	configPathTOML := filepath.Join(path, configDirPath, configFileTOML)
	bytes, err = os.ReadFile(configPathTOML)
	if err == nil {
		return loadConfig(bytes, isDryRun, toml.Unmarshal)
	}

	return nil, ErrConfigNotFound
}

func loadConfig(bytes []byte, isDryRun bool, unmarshalFn func(data []byte, v any) error) (Config, error) {
	var cfgApps ConfigApps
	if err := unmarshalFn(bytes, &cfgApps); err != nil {
		return nil, fmt.Errorf("failed to unmarshal: %w", err)
	}

	// Sort version
	apps2 := make([]string, 0, len(cfgApps.Apps))

	for appName := range cfgApps.Apps {
		apps2 = append(apps2, appName)
	}

	sort.Strings(apps2)
	cfgApps.Apps2 = apps2

	return &cfg{
		cfgApps:  cfgApps,
		isDryRun: isDryRun,
	}, nil
}

// Install internal -> external
func (c *cfg) Install(appNames ...string) error {
	var eg errgroup.Group

	mAppNames := slice2map(appNames)

	for appName, app := range c.cfgApps.Apps {
		if len(appNames) > 0 {
			if _, ok := mAppNames[appName]; !ok {
				continue
			}
		}

		for _, p := range app.Paths {
			if p.External == "" {
				continue
			}

			p := Path{
				Internal: p.Internal,
				External: p.External,
				URL:      p.URL,
			}

			eg.Go(func() error {
				if c.isDryRun {
					fmt.Printf("Replace [%s] -> [%s]\n", p.Internal, p.External)
					return nil
				}

				if err := copy.Replace(p.Internal, p.External); err != nil {
					return fmt.Errorf("copy: failed to replace [%s] -> [%s]: %w", p.Internal, p.External, err)
				}

				return nil
			})
		}
	}

	if err := eg.Wait(); err != nil {
		return err
	}

	return nil
}

// Update external -> internal
func (c *cfg) Update(appNames ...string) error {
	var eg errgroup.Group

	mAppNames := slice2map(appNames)

	for appName, app := range c.cfgApps.Apps {
		if len(appNames) > 0 {
			if _, ok := mAppNames[appName]; !ok {
				continue
			}
		}

		for _, p := range app.Paths {
			if p.External == "" {
				continue
			}

			p := Path{
				Internal: p.Internal,
				External: p.External,
				URL:      p.URL,
			}

			eg.Go(func() error {
				if c.isDryRun {
					fmt.Printf("Replace [%s] -> [%s]\n", p.External, p.Internal)
					return nil
				}

				if err := copy.Replace(p.External, p.Internal); err != nil {
					return fmt.Errorf("copy: failed to replace [%s] -> [%s]: %w", p.External, p.Internal, err)
				}

				return nil
			})
		}
	}

	if err := eg.Wait(); err != nil {
		return err
	}

	return nil
}

func (c *cfg) Download(appNames ...string) error {
	var eg errgroup.Group

	mAppNames := slice2map(appNames)

	for appName, app := range c.cfgApps.Apps {
		if len(appNames) > 0 {
			if _, ok := mAppNames[appName]; !ok {
				continue
			}
		}

		for _, p := range app.Paths {
			if p.URL == "" {
				continue
			}

			p := Path{
				Internal: p.Internal,
				External: p.External,
				URL:      p.URL,
			}

			eg.Go(func() error {
				if c.isDryRun {
					fmt.Printf("Download [%s] -> [%s]\n", p.URL, p.Internal)
					return nil
				}

				// nolint:noctx,gosec
				httpRsp, err := http.Get(p.URL)
				if err != nil {
					return fmt.Errorf("http client: failed to get: %w", err)
				}

				data, err := io.ReadAll(httpRsp.Body)
				if err != nil {
					return fmt.Errorf("io: failed to read all: %w", err)
				}

				// Copy from github.com/make-go-great/copy-go
				// Make sure nested dir is exist before copying file
				dstDir := filepath.Dir(p.Internal)
				if err := os.MkdirAll(dstDir, os.ModePerm); err != nil {
					return fmt.Errorf("os: failed to mkdir all [%s]: %w", dstDir, err)
				}

				if err := os.WriteFile(p.Internal, data, 0o600); err != nil {
					return fmt.Errorf("os: failed to write file: %w", err)
				}

				httpRsp.Body.Close()

				return nil
			})

		}
	}

	if err := eg.Wait(); err != nil {
		return err
	}

	return nil
}

// Clean remove unused config inside config dir
func (c *cfg) Clean() error {
	unusedDirs, err := getUnusedDirs(c.cfgApps.Apps)
	if err != nil {
		return err
	}

	// Delete ununsed dirs to save some space
	for dir := range unusedDirs {
		if c.isDryRun {
			fmt.Printf("Remove [%s]\n", dir)
			continue
		}

		dirPath := filepath.Join(configDirPath, dir)
		if err := os.RemoveAll(dirPath); err != nil {
			return fmt.Errorf("os: failed to remove all [%s]: %w", dir, err)
		}
	}

	return nil
}

func getUnusedDirs(apps map[string]App) (map[string]struct{}, error) {
	files, err := os.ReadDir(configDirPath)
	if err != nil {
		return nil, fmt.Errorf("os: failed to read dir [%s]: %w", configDirPath, err)
	}

	// Get all dirs inside config dir
	unusedDirs := make(map[string]struct{})
	for _, file := range files {
		// Ignore config file
		if file.Name() == configFileJSON {
			continue
		}

		unusedDirs[file.Name()] = struct{}{}
	}

	// Removed used dirs
	for name := range apps {
		delete(unusedDirs, name)
	}

	return unusedDirs, nil
}

func (c *cfg) Diff(appNames ...string) error {
	mAppNames := slice2map(appNames)

	for appName, app := range c.cfgApps.Apps {
		if len(appNames) > 0 {
			if _, ok := mAppNames[appName]; !ok {
				continue
			}
		}

		for _, p := range app.Paths {
			if p.External == "" {
				continue
			}

			if err := diff.Diff(p.Internal, p.External); err != nil {
				return fmt.Errorf("diff: failed to compare [%s] with [%s]: %w", p.Internal, p.External, err)
			}
		}
	}

	return nil
}

func (c *cfg) Validate(appNames ...string) error {
	var eg errgroup.Group

	mAppNames := slice2map(appNames)

	for appName, app := range c.cfgApps.Apps {
		if len(appNames) > 0 {
			if _, ok := mAppNames[appName]; !ok {
				continue
			}
		}

		for _, p := range app.Paths {
			app := app
			p := Path{
				Internal: p.Internal,
				External: p.External,
				URL:      p.URL,
			}

			eg.Go(func() error {
				if p.Internal == "" {
					return fmt.Errorf("empty internal app [%s]: %w", app, ErrConfigInvalid)
				}

				if p.External == "" && p.URL == "" {
					return fmt.Errorf("empty external and url app [%s]: %w", app, ErrConfigInvalid)
				}

				return nil
			})
		}
	}

	if err := eg.Wait(); err != nil {
		return err
	}

	return nil
}

func (c *cfg) List() []string {
	return c.cfgApps.Apps2
}
