package config

import (
	"errors"
	"fmt"
	"io"
	"os"
	"path"
	"path/filepath"
	"strconv"

	"git.gabrielgio.me/cerrado/pkg/u"
	"git.sr.ht/~emersion/go-scfg"
)

var (
	ErrScanPath        = errors.New("Scan path does not exist")
	ErrRepoPath        = errors.New("Repository path does not exist")
	ErrInvalidProperty = errors.New("Invalid property")
)

type (

	// scan represents piece of the scan from the configuration file.
	scan struct {
		Path   string
		Public bool
	}

	// configuration represents file configuration.
	// fields needs to be exported to cmp to work
	configuration struct {
		Scan            *scan
		RootReadme      string
		ListenAddr      string
		Passphrase      string
		SyntaxHighlight string
		AESKey          string
		Repositories    []*GitRepositoryConfiguration
	}

	// This is a per repository configuration.
	GitRepositoryConfiguration struct {
		Name        string
		Path        string
		Description string
		Public      bool
		About       string
	}

	// ConfigurationRepository represents the configuration repository (as in
	// database repositories).
	// This holds all the function necessary to ask for configuration
	// information.
	ConfigurationRepository struct {
		rootReadme      string
		listenAddr      string
		passphrase      []byte
		aesKey          []byte
		syntaxHighlight string
		repositories    []*GitRepositoryConfiguration
	}
)

func LoadConfigurationRepository(configPath string) (*ConfigurationRepository, error) {
	f, err := os.Open(configPath)
	if err != nil {
		return nil, err
	}

	config, err := parse(f)
	if err != nil {
		return nil, err
	}

	repo := &ConfigurationRepository{
		aesKey:          []byte(config.AESKey),
		listenAddr:      config.ListenAddr,
		passphrase:      []byte(config.Passphrase),
		repositories:    config.Repositories,
		rootReadme:      config.RootReadme,
		syntaxHighlight: config.SyntaxHighlight,
	}

	if config.Scan.Path != "" {
		err = repo.expandOnScanPath(config.Scan.Path, config.Scan.Public)
		if err != nil {
			return nil, err
		}
	}

	return repo, nil
}

// GetRootReadme returns root read path
func (c *ConfigurationRepository) GetRootReadme() string {
	return c.rootReadme
}

func (c *ConfigurationRepository) GetSyntaxHighlight() string {
	return c.syntaxHighlight
}

func (c *ConfigurationRepository) GetListenAddr() string {
	return c.listenAddr
}

func (c *ConfigurationRepository) GetPassphrase() []byte {
	return c.passphrase
}

func (c *ConfigurationRepository) GetBase64AesKey() []byte {
	return c.aesKey
}

func (c *ConfigurationRepository) IsAuthEnabled() bool {
	return len(c.passphrase) != 0
}

// GetByName returns configuration of repository for a given name.
// It returns nil if there is not match for it.
func (c *ConfigurationRepository) GetByName(name string) *GitRepositoryConfiguration {
	for _, r := range c.repositories {
		if r.Name == name {
			return r
		}
	}
	return nil
}

// List returns all the configuration for all repositories.
func (c *ConfigurationRepository) List() []*GitRepositoryConfiguration {
	return c.repositories
}

// expandOnScanPath scans the scanPath for folders taking them as repositories
// and applying them default configuration.
func (c *ConfigurationRepository) expandOnScanPath(scanPath string, public bool) error {
	if !u.FileExist(scanPath) {
		return ErrScanPath
	}

	entries, err := os.ReadDir(scanPath)
	if err != nil {
		return err
	}

	for _, e := range entries {
		if !e.IsDir() {
			continue
		}

		fullPath := path.Join(scanPath, e.Name())
		if !c.repoExits(fullPath) {
			c.repositories = append(c.repositories, &GitRepositoryConfiguration{
				Name:   e.Name(),
				Path:   fullPath,
				Public: public,
			})
		}
	}
	return nil
}

func (c *ConfigurationRepository) repoExits(path string) bool {
	for _, r := range c.repositories {
		if path == r.Path {
			return true
		}
	}
	return false
}

func parse(r io.Reader) (*configuration, error) {
	block, err := scfg.Read(r)
	if err != nil {
		return nil, err
	}

	config := defaultConfiguration()

	err = setScan(block, config.Scan)
	if err != nil {
		return nil, err
	}

	err = setRootReadme(block, &config.RootReadme)
	if err != nil {
		return nil, err
	}

	err = setListenAddr(block, &config.ListenAddr)
	if err != nil {
		return nil, err
	}

	err = setPassphrase(block, &config.Passphrase)
	if err != nil {
		return nil, err
	}

	err = setAESKey(block, &config.AESKey)
	if err != nil {
		return nil, err
	}

	err = setSyntaxHighlight(block, &config.SyntaxHighlight)
	if err != nil {
		return nil, err
	}

	err = setRepositories(block, &config.Repositories)
	if err != nil {
		return nil, err
	}

	return config, nil
}

func setRepositories(block scfg.Block, repositories *[]*GitRepositoryConfiguration) error {
	blocks := block.GetAll("repository")

	for _, r := range blocks {
		if len(r.Params) != 1 {
			return fmt.Errorf(
				"Invlid number of params for repository: %w",
				ErrInvalidProperty,
			)
		}

		path := u.FirstOrZero(r.Params)
		repository := defaultRepisotryConfiguration(path)

		for _, d := range r.Children {
			// under repository there is only single param properties
			if len(d.Params) != 1 {
				return fmt.Errorf(
					"Invlid number of params for %s: %w",
					d.Name,
					ErrInvalidProperty,
				)
			}

			switch d.Name {
			case "name":
				if err := setString(d, &repository.Name); err != nil {
					return err
				}
			case "description":
				if err := setString(d, &repository.Description); err != nil {
					return err
				}
			case "public":
				if err := setBool(d, &repository.Public); err != nil {
					return err
				}
			case "about":
				if err := setString(d, &repository.About); err != nil {
					return err
				}
			}
		}

		*repositories = append(*repositories, repository)
	}

	return nil
}

func defaultConfiguration() *configuration {
	return &configuration{
		Scan:         defaultScan(),
		RootReadme:   "",
		ListenAddr:   defaultAddr(),
		Repositories: make([]*GitRepositoryConfiguration, 0),
	}
}

func defaultScan() *scan {
	return &scan{
		Public: false,
		Path:   "",
	}
}

func defaultAddr() string {
	return "tcp://localhost:8080"
}

func defaultRepisotryConfiguration(path string) *GitRepositoryConfiguration {
	return &GitRepositoryConfiguration{
		Path:        path,
		Name:        filepath.Base(path),
		Description: "",
		Public:      false,
		About:       "README.md",
	}
}

func setRootReadme(block scfg.Block, readme *string) error {
	scanDir := block.Get("root-readme")
	return setString(scanDir, readme)
}

func setPassphrase(block scfg.Block, listenAddr *string) error {
	scanDir := block.Get("passphrase")
	return setString(scanDir, listenAddr)
}

func setAESKey(block scfg.Block, listenAddr *string) error {
	scanDir := block.Get("aes-key")
	return setString(scanDir, listenAddr)
}

func setSyntaxHighlight(block scfg.Block, listenAddr *string) error {
	scanDir := block.Get("syntax-highlight")
	return setString(scanDir, listenAddr)
}

func setListenAddr(block scfg.Block, listenAddr *string) error {
	scanDir := block.Get("listen-addr")
	return setString(scanDir, listenAddr)
}

func setScan(block scfg.Block, scan *scan) error {
	scanDir := block.Get("scan")
	if scanDir == nil {
		return nil
	}
	err := setString(scanDir, &scan.Path)
	if err != nil {
		return err
	}

	public := scanDir.Children.Get("public")
	return setBool(public, &scan.Public)
}

func setBool(dir *scfg.Directive, field *bool) error {
	if dir != nil {

		p1, _ := u.First(dir.Params)
		v, err := strconv.ParseBool(p1)
		if err != nil {
			return fmt.Errorf("Error parsing bool param of %s: %w", dir.Name, err)
		}
		*field = v
	}
	return nil
}

func setString(dir *scfg.Directive, field *string) error {
	if dir != nil {
		*field = u.FirstOrZero(dir.Params)
	}
	return nil
}