package config

import (
	"encoding/json"
	"errors"
	"fmt"
	"io/fs"
	"os"
	"path/filepath"

	"github.com/containers/storage/pkg/ioutils"
	"github.com/containers/storage/pkg/lockfile"
)

const connectionsFile = "podman-connections.json"

// connectionsConfigFile returns the path to the rw connections config file
func connectionsConfigFile() (string, error) {
	if path, found := os.LookupEnv("PODMAN_CONNECTIONS_CONF"); found {
		return path, nil
	}
	path, err := userConfigPath()
	if err != nil {
		return "", err
	}
	// file is stored next to containers.conf
	return filepath.Join(filepath.Dir(path), connectionsFile), nil
}

type ConnectionConfig struct {
	Default     string                 `json:",omitempty"`
	Connections map[string]Destination `json:",omitempty"`
}

type ConnectionsFile struct {
	Connection ConnectionConfig `json:",omitempty"`
	Farm       FarmConfig       `json:",omitempty"`
}

type Connection struct {
	// Name of the connection
	Name string

	// Destination for this connection
	Destination

	// Default if this connection is the default
	Default bool

	// ReadWrite if true the connection is stored in the connections file
	ReadWrite bool
}

type Farm struct {
	// Name of the farm
	Name string

	// Connections
	Connections []string

	// Default if this is the default farm
	Default bool

	// ReadWrite if true the farm is stored in the connections file
	ReadWrite bool
}

func readConnectionConf(path string) (*ConnectionsFile, error) {
	conf := new(ConnectionsFile)
	f, err := os.Open(path)
	if err != nil {
		// return empty config if file does not exists
		if errors.Is(err, fs.ErrNotExist) {
			return conf, nil
		}

		return nil, err
	}
	defer f.Close()

	err = json.NewDecoder(f).Decode(conf)
	if err != nil {
		return nil, fmt.Errorf("parse %q: %w", path, err)
	}
	return conf, nil
}

func writeConnectionConf(path string, conf *ConnectionsFile) error {
	if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
		return err
	}

	opts := &ioutils.AtomicFileWriterOptions{ExplicitCommit: true}
	configFile, err := ioutils.NewAtomicFileWriterWithOpts(path, 0o644, opts)
	if err != nil {
		return err
	}
	defer configFile.Close()

	err = json.NewEncoder(configFile).Encode(conf)
	if err != nil {
		return err
	}

	// If no errors commit the changes to the config file
	return configFile.Commit()
}

// EditConnectionConfig must be used to edit the connections config.
// The function will read and write the file automatically and the
// callback function just needs to modify the cfg as needed.
func EditConnectionConfig(callback func(cfg *ConnectionsFile) error) error {
	path, err := connectionsConfigFile()
	if err != nil {
		return err
	}

	lockPath := path + ".lock"
	lock, err := lockfile.GetLockFile(lockPath)
	if err != nil {
		return fmt.Errorf("obtain lock file: %w", err)
	}
	lock.Lock()
	defer lock.Unlock()

	conf, err := readConnectionConf(path)
	if err != nil {
		return fmt.Errorf("read connections file: %w", err)
	}
	if conf.Farm.List == nil {
		conf.Farm.List = make(map[string][]string)
	}

	if err := callback(conf); err != nil {
		return err
	}

	return writeConnectionConf(path, conf)
}

func makeConnection(name string, dst Destination, def, readWrite bool) *Connection {
	return &Connection{
		Name:        name,
		Destination: dst,
		Default:     def,
		ReadWrite:   readWrite,
	}
}

// GetConnection return the connection for the given name or if def is set to true then return the default connection.
func (c *Config) GetConnection(name string, def bool) (*Connection, error) {
	path, err := connectionsConfigFile()
	if err != nil {
		return nil, err
	}
	conConf, err := readConnectionConf(path)
	if err != nil {
		return nil, err
	}
	defaultCon := conConf.Connection.Default
	if defaultCon == "" {
		defaultCon = c.Engine.ActiveService
	}
	if def {
		if defaultCon == "" {
			return nil, errors.New("no default connection found")
		}
		name = defaultCon
	} else {
		def = defaultCon == name
	}

	if dst, ok := conConf.Connection.Connections[name]; ok {
		return makeConnection(name, dst, def, true), nil
	}
	if dst, ok := c.Engine.ServiceDestinations[name]; ok {
		return makeConnection(name, dst, def, false), nil
	}
	return nil, fmt.Errorf("connection %q not found", name)
}

// GetAllConnections return all configured connections
func (c *Config) GetAllConnections() ([]Connection, error) {
	path, err := connectionsConfigFile()
	if err != nil {
		return nil, err
	}
	conConf, err := readConnectionConf(path)
	if err != nil {
		return nil, err
	}

	defaultCon := conConf.Connection.Default
	if defaultCon == "" {
		defaultCon = c.Engine.ActiveService
	}

	connections := make([]Connection, 0, len(conConf.Connection.Connections))
	for name, dst := range conConf.Connection.Connections {
		def := defaultCon == name
		connections = append(connections, *makeConnection(name, dst, def, true))
	}
	for name, dst := range c.Engine.ServiceDestinations {
		if _, ok := conConf.Connection.Connections[name]; ok {
			// connection name is overwritten by connections file
			continue
		}
		def := defaultCon == name
		connections = append(connections, *makeConnection(name, dst, def, false))
	}

	return connections, nil
}

func getConnections(cons []string, dests map[string]Destination) ([]Connection, error) {
	connections := make([]Connection, 0, len(cons))
	for _, name := range cons {
		if dst, ok := dests[name]; ok {
			connections = append(connections, *makeConnection(name, dst, false, false))
		} else {
			return nil, fmt.Errorf("connection %q not found", name)
		}
	}
	return connections, nil
}

// GetFarmConnections return all the connections for the given farm.
func (c *Config) GetFarmConnections(name string) ([]Connection, error) {
	_, cons, err := c.getFarmConnections(name, false)
	return cons, err
}

// GetDefaultFarmConnections returns the name of the default farm
// and the connections.
func (c *Config) GetDefaultFarmConnections() (string, []Connection, error) {
	return c.getFarmConnections("", true)
}

// getFarmConnections returns all connections for the given farm,
// if def is true it will use the default farm instead of the name.
// Returns the name of the farm and the connections for it.
func (c *Config) getFarmConnections(name string, def bool) (string, []Connection, error) {
	path, err := connectionsConfigFile()
	if err != nil {
		return "", nil, err
	}
	conConf, err := readConnectionConf(path)
	if err != nil {
		return "", nil, err
	}
	defaultFarm := conConf.Farm.Default
	if defaultFarm == "" {
		defaultFarm = c.Farms.Default
	}
	if def {
		if defaultFarm == "" {
			return "", nil, errors.New("no default farm found")
		}
		name = defaultFarm
	}

	if cons, ok := conConf.Farm.List[name]; ok {
		cons, err := getConnections(cons, conConf.Connection.Connections)
		return name, cons, err
	}
	if cons, ok := c.Farms.List[name]; ok {
		cons, err := getConnections(cons, c.Engine.ServiceDestinations)
		return name, cons, err
	}
	return "", nil, fmt.Errorf("farm %q not found", name)
}

func makeFarm(name string, cons []string, def, readWrite bool) Farm {
	return Farm{
		Name:        name,
		Connections: cons,
		Default:     def,
		ReadWrite:   readWrite,
	}
}

// GetAllFarms returns all configured farms
func (c *Config) GetAllFarms() ([]Farm, error) {
	path, err := connectionsConfigFile()
	if err != nil {
		return nil, err
	}
	conConf, err := readConnectionConf(path)
	if err != nil {
		return nil, err
	}
	defaultFarm := conConf.Farm.Default
	if defaultFarm == "" {
		defaultFarm = c.Farms.Default
	}

	farms := make([]Farm, 0, len(conConf.Farm.List))
	for name, cons := range conConf.Farm.List {
		def := defaultFarm == name
		farms = append(farms, makeFarm(name, cons, def, true))
	}
	for name, cons := range c.Farms.List {
		if _, ok := conConf.Farm.List[name]; ok {
			// farm name is overwritten by connections file
			continue
		}
		def := defaultFarm == name
		farms = append(farms, makeFarm(name, cons, def, false))
	}

	return farms, nil
}
