package config

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

	"github.com/dexidp/dex/connector/oidc"
	"github.com/kelseyhightower/envconfig"
	"go.uber.org/zap"
)

const (
	envConfigPrefix   = "POLYCULECONNECT"
	DefaultConfigPath = "/etc/polyculeconnect.json"
)

type ListeningMode string

const (
	ModeUnix ListeningMode = "unix"
	ModeNet  ListeningMode = "net"
)

type BackendConfigType string

const (
	Memory BackendConfigType = "memory"
	SQLite BackendConfigType = "sqlite"
)

const (
	defaultLogLevel = zap.InfoLevel

	defaultServerMode      = ModeNet
	defaultServerHost      = "0.0.0.0"
	defaultServerPort      = 5000
	defaultServerSocket    = ""
	defaultServerStaticDir = "./"

	defaultIssuer = "http://localhost:5000"

	defaultStorageType      = Memory
	defaultStorageFile      = "./polyculeconnect.db"
	defaultStorageHost      = "127.0.0.1"
	defaultStoragePort      = 5432
	defaultStorageDB        = "polyculeconnect"
	defaultStorageUser      = "polyculeconnect"
	defaultStoragePassword  = "polyculeconnect"
	defaultStorageSSLMode   = "disable"
	defaultStorageSSLCaFile = ""
)

// Deprecated: remove when we finally drop the JSON config
type BackendConfig struct {
	Config *oidc.Config      `json:"config"`
	Name   string            `json:"name"`
	ID     string            `json:"ID"`
	Type   BackendConfigType `json:"type"`
	Local  bool              `json:"local"`
}

type SSLStorageConfig struct {
	Mode   string `json:"mode" envconfig:"STORAGE_SSL_MODE"`
	CaFile string `json:"ca_file" envconfig:"STORAGE_SSL_CA_FILE"`
}

type StorageConfig struct {
	File     string `envconfig:"STORAGE_PATH"`
	Host     string `envconfig:"STORAGE_HOST"`
	Port     int    `envconfig:"STORAGE_PORT"`
	Database string `envconfig:"STORAGE_DATABASE"`
	User     string `envconfig:"STORAGE_USER"`
	Password string `envconfig:"STORAGE_PASSWORD"`
	SSL      SSLStorageConfig
}

type logConfig struct {
	Level string `json:"level"`
}

type serverConfig struct {
	Mode string `json:"mode"`
	Host string `json:"host"`
	Port int    `json:"port"`
	Sock string `json:"sock"`
}

type jsonStorageConfig struct {
	Type     string           `json:"type"`
	File     string           `json:"path"`
	Host     string           `json:"host" `
	Port     int              `json:"port"`
	Database string           `json:"database" `
	User     string           `json:"user" `
	Password string           `json:"password" `
	SSL      SSLStorageConfig `json:"ssl"`
}

type jsonConf struct {
	LogConfig     logConfig         `json:"log"`
	ServerConfig  serverConfig      `json:"server"`
	StorageConfig jsonStorageConfig `json:"storage"`
}

func (c *jsonConf) initValues(ac *AppConfig) {
	c.LogConfig = logConfig{Level: ac.LogLevel.String()}
	c.ServerConfig = serverConfig{
		Mode: string(ac.ServerMode),
		Host: ac.Host,
		Port: ac.Port,
		Sock: ac.SockPath,
	}
	c.StorageConfig = jsonStorageConfig{
		Type:     ac.StorageType,
		File:     ac.StorageConfig.File,
		Host:     ac.StorageConfig.Host,
		Port:     ac.StorageConfig.Port,
		Database: ac.StorageConfig.Database,
		User:     ac.StorageConfig.User,
		Password: ac.StorageConfig.Password,
		SSL:      ac.StorageConfig.SSL,
	}
}

type AppConfig struct {
	LogLevel      zap.AtomicLevel `envconfig:"LOG_LEVEL"`
	ServerMode    ListeningMode   `envconfig:"SERVER_MODE"`
	Host          string          `envconfig:"SERVER_HOST"`
	Port          int             `envconfig:"SERVER_PORT"`
	SockPath      string          `envconfig:"SERVER_SOCK"`
	StorageType   string          `envconfig:"STORAGE_TYPE"`
	StorageConfig *StorageConfig
	Issuer        string
	StaticDir     string
}

func defaultConfig() AppConfig {
	return AppConfig{
		LogLevel:    zap.NewAtomicLevelAt(defaultLogLevel),
		ServerMode:  defaultServerMode,
		Host:        defaultServerHost,
		Port:        defaultServerPort,
		SockPath:    defaultServerSocket,
		StorageType: string(defaultStorageType),
		StorageConfig: &StorageConfig{
			File:     defaultStorageFile,
			Host:     defaultStorageHost,
			Port:     defaultServerPort,
			Database: defaultStorageDB,
			User:     defaultStorageUser,
			Password: defaultStoragePassword,
			SSL: SSLStorageConfig{
				Mode:   defaultStorageSSLMode,
				CaFile: defaultStorageSSLCaFile,
			},
		},
		Issuer:    defaultIssuer,
		StaticDir: defaultServerStaticDir,
	}
}

func parseLevel(lvlStr string) zap.AtomicLevel {
	var res zap.AtomicLevel
	if err := res.UnmarshalText([]byte(lvlStr)); err != nil {
		return zap.NewAtomicLevelAt(zap.InfoLevel)
	}
	return res
}

func (ac *AppConfig) UnmarshalJSON(data []byte) error {
	var jsonConf jsonConf
	jsonConf.initValues(ac)

	if err := json.Unmarshal(data, &jsonConf); err != nil {
		return fmt.Errorf("failed to read JSON: %w", err)
	}

	ac.LogLevel = parseLevel(jsonConf.LogConfig.Level)

	ac.ServerMode = ListeningMode(jsonConf.ServerConfig.Mode)
	ac.Host = jsonConf.ServerConfig.Host
	ac.Port = jsonConf.ServerConfig.Port
	ac.SockPath = jsonConf.ServerConfig.Sock

	ac.StorageType = jsonConf.StorageConfig.Type
	ac.StorageConfig.File = jsonConf.StorageConfig.File
	ac.StorageConfig.Host = jsonConf.StorageConfig.Host
	ac.StorageConfig.Port = jsonConf.StorageConfig.Port
	ac.StorageConfig.Database = jsonConf.StorageConfig.Database
	ac.StorageConfig.User = jsonConf.StorageConfig.User
	ac.StorageConfig.Password = jsonConf.StorageConfig.Password
	ac.StorageConfig.SSL = jsonConf.StorageConfig.SSL
	return nil
}

func (ac *AppConfig) RedirectURI() string {
	return ac.Issuer + "/callback"
}

func New(filepath string) (*AppConfig, error) {
	conf := defaultConfig()
	content, err := os.ReadFile(filepath)
	if err != nil {
		if !errors.Is(err, fs.ErrNotExist) {
			return nil, fmt.Errorf("failed to read config file %q: %w", filepath, err)
		}
	} else {
		if err := json.Unmarshal(content, &conf); err != nil {
			return nil, fmt.Errorf("failed to parse config file: %w", err)
		}
	}
	if err := envconfig.Process(envConfigPrefix, &conf); err != nil {
		return nil, err
	}
	if err := envconfig.Process(envConfigPrefix, conf.StorageConfig); err != nil {
		return nil, err
	}
	if err := envconfig.Process(envConfigPrefix, &conf.StorageConfig.SSL); err != nil {
		return nil, err
	}
	return &conf, nil
}
