package config

import (
	"encoding/json"
	"fmt"
	"io/fs"
	"net"
	"os"
	"strconv"

	"github.com/go-errors/errors"

	"github.com/phishingclub/phishingclub/data"
	"github.com/phishingclub/phishingclub/errs"
	"github.com/phishingclub/phishingclub/file"
)

var (
	ErrMissingIP          = errors.New("missing IP")
	ErrMissingPort        = errors.New("missing port")
	ErrMissingDatabaseDSN = errors.New("missing database DSN")
	ErrInvalidIP          = errors.New("invalid IP")
	ErrInvalidPort        = errors.New("invalid port")
	ErrInvalidDatabase    = errors.New("invalid database")
	ErrWriterIsNil        = errors.New("writer is nil")
)

const (
	DefaultACMEEmail    = ""
	DefaultDevACMEEmail = ""

	DatabaseUsePostgres            = "postgres"
	DefaultAdministrationUseSqlite = "sqlite3"
	DefaultDatabase                = DefaultAdministrationUseSqlite
	DefaultAdministrationDSN       = "file:./db.sqlite3"

	DefaultDevAdministrationPort = 0 // 0 uses ephemeral port, random available port
	DefaultDevHTTPPhishingPort   = 8080
	DefaultDevHTTPSPhishingPort  = 8443

	DefaultProductionAdministrationPort = 0 // 0 uses ephemeral port, random available port
	DefaultProductionHTTPPhishingPort   = 80
	DefaultProductionHTTPSPhishingPort  = 443

	// empty is none
	DefaultLogFilePath    = ""
	DefaultErrLogFilePath = ""

	DefaultTrustedIPHeader = ""

	DefaultAdminHost          = ""
	DefaultAdminAutoTLS       = true
	DefaultAdminAutoTLSString = "true"
)

var (
	defaultTrustedProxies = []string{}
	defaultAdminAllowed   = []string{}
)

type (
	// Config config
	Config struct {
		acme ACME

		tlsHost string
		tlsAuto bool

		tlsCertPath string
		tlsKeyPath  string

		adminNetAddress         net.TCPAddr
		phishingHTTPNetAddress  net.TCPAddr
		phishingHTTPSNetAddress net.TCPAddr
		database                Database
		fileWriter              file.Writer

		LogPath    string
		ErrLogPath string

		IPSecurity     IPSecurityConfig
		RemoteBrowser  RemoteBrowserServerConfig
		Authentication AuthenticationConfig
	}

	// ConfigDTO config DTO
	ConfigDTO struct {
		ACME                 ACME                      `json:"acme"`
		AdministrationServer AdministrationServer      `json:"administration"`
		PhishingServer       PhishingServer            `json:"phishing"`
		Database             Database                  `json:"database"`
		Log                  Log                       `json:"log"`
		IPSecurity           IPSecurityConfig          `json:"ip_security"`
		RemoteBrowser        RemoteBrowserServerConfig `json:"remote_browser"`
		Authentication       AuthenticationConfig      `json:"authentication"`
	}

	// AuthenticationConfig holds server level authentication settings.
	AuthenticationConfig struct {
		// LocalLoginBreakglass keeps username and password login available even
		// when exclusive SSO is enabled. Set at the server level only as a
		// recovery path if the identity provider is misconfigured and locks
		// everyone out. Defaults to false.
		LocalLoginBreakglass bool `json:"local_login_breakglass"`
	}

	Log struct {
		Path      string `json:"path"`
		ErrorPath string `json:"errorPath"`
	}

	// AdministrationServer ConfigDTO administration
	AdministrationServer struct {
		TLSHost     string `json:"tls_host"`
		TLSAuto     bool   `json:"tls_auto"`
		TLSCertPath string `json:"tls_cert_path"`
		TLSKeyPath  string `json:"tls_key_path"`
		Address     string `json:"address"`
	}

	// PhishingServer ConfigDTO phishing
	PhishingServer struct {
		Http  string `json:"http"`
		Https string `json:"https"`
	}

	//  Database ConfigDTO database
	Database struct {
		Engine string `json:"engine"`
		DSN    string `json:"dsn"`
	}

	// ACME ConfigDTO acme
	ACME struct {
		Email string `json:"email"`
	}

	// RemoteBrowserServerConfig holds server-side remote browser settings.
	RemoteBrowserServerConfig struct {
		// Enabled controls whether the remote browser feature is available.
		// Defaults to false. When false all remote browser endpoints return 404.
		// When true all built-in safeguards are removed (Chrome flag blocklist,
		// URL scheme restriction) because operators are trusted at server level.
		// Only enable on instances where every operator is trusted as a server admin.
		Enabled bool `json:"enabled"`
		// ExecPath is the path to a Chrome/Chromium binary. When empty Rod uses
		// its own auto-downloaded Chromium. Set at the server level only.
		ExecPath string `json:"exec_path"`
	}
)

type IPSecurityConfig struct {
	// ip/cidr that are allowed to access the admin interface
	AdminAllowed []string `json:"admin_allowed"`

	// ip/cidr of legitimate reverse proxies (e.g., Nginx, HAProxy, Cloudflare edges)
	TrustedProxies []string `json:"trusted_proxies"`

	// headers to check for real client IP
	// examples: CF-Connecting-IP, X-Real-IP, True-Client-IP, X-Forwarded-For
	TrustedIPHeader string `json:"trusted_ip_header"`
}

// ValidateFileWriter validates the file writer
func ValidateFileWriter(fileWriter file.Writer) error {
	if fileWriter == nil {
		return ErrWriterIsNil
	}
	return nil
}

// NewConfig factory
func NewConfig(
	acmeEmail string,
	tlsHost string,
	tlsAuto bool,
	adminPublicCertPath string,
	adminPrivateCertKey string,
	adminAddress string,
	phishingHTTPAddress string,
	phishingHTTPSAddress string,
	database Database,
	fileWriter file.Writer,
	logPath string,
	errLogPath string,
	ipSecurity IPSecurityConfig,
) (*Config, error) {
	if err := ValidateFileWriter(fileWriter); err != nil {
		return nil, errs.Wrap(err)
	}
	adminNetAddress, err := StringAddressToTCPAddr(adminAddress)
	if err != nil {
		return nil, errs.Wrap(err)
	}
	phishingHTTPNetAddress, err := StringAddressToTCPAddr(phishingHTTPAddress)
	if err != nil {
		return nil, errs.Wrap(err)
	}
	phishingHTTPSNetAddress, err := StringAddressToTCPAddr(phishingHTTPSAddress)
	if err != nil {
		return nil, errs.Wrap(err)
	}
	switch database.Engine {
	case DatabaseUsePostgres:
	case DefaultAdministrationUseSqlite:
	default:
		return nil, ErrInvalidDatabase
	}

	return &Config{
		acme: ACME{
			Email: acmeEmail,
		},
		tlsHost:                 tlsHost,
		tlsAuto:                 tlsAuto,
		tlsCertPath:             adminPublicCertPath,
		tlsKeyPath:              adminPrivateCertKey,
		adminNetAddress:         *adminNetAddress,
		phishingHTTPNetAddress:  *phishingHTTPNetAddress,
		phishingHTTPSNetAddress: *phishingHTTPSNetAddress,
		database: Database{
			Engine: database.Engine,
			DSN:    database.DSN,
		},
		fileWriter: &file.FileWriter{},
		LogPath:    logPath,
		ErrLogPath: errLogPath,
		IPSecurity: ipSecurity,
	}, nil
}

// NewDevDefaultConfig returns a default config
func NewDevDefaultConfig() *Config {
	tlsHost := "phish.test"
	tlsAuto := false
	publicCertPath := fmt.Sprintf(
		"%s/%s",
		data.DefaultAdminCertDir,
		data.DefaultAdminPublicCertFileName,
	)
	privateCertPath := fmt.Sprintf(
		"%s/%s",
		data.DefaultAdminCertDir,
		data.DefaultAdminPrivateCertFileName,
	)
	return &Config{
		acme: ACME{
			Email: DefaultACMEEmail,
		},
		tlsHost:     tlsHost,
		tlsAuto:     tlsAuto,
		tlsCertPath: publicCertPath,
		tlsKeyPath:  privateCertPath,
		adminNetAddress: net.TCPAddr{
			IP:   net.IPv4(0, 0, 0, 0),
			Port: DefaultDevAdministrationPort,
		},
		phishingHTTPNetAddress: net.TCPAddr{
			IP:   net.IPv4(0, 0, 0, 0),
			Port: DefaultDevHTTPPhishingPort,
		},
		phishingHTTPSNetAddress: net.TCPAddr{
			IP:   net.IPv4(0, 0, 0, 0),
			Port: DefaultDevHTTPSPhishingPort,
		},
		database: Database{
			Engine: DefaultAdministrationUseSqlite,
			DSN:    DefaultAdministrationDSN,
		},
		fileWriter: &file.FileWriter{},
		LogPath:    DefaultLogFilePath,
		ErrLogPath: DefaultErrLogFilePath,
		IPSecurity: IPSecurityConfig{
			AdminAllowed:    []string{},
			TrustedProxies:  []string{},
			TrustedIPHeader: "",
		},
	}
}

// NewDevDefaultConfig returns a default config
func NewProductionDefaultConfig() *Config {
	tlsHost := "localhost"
	tlsAuto := DefaultAdminAutoTLS
	publicCertPath := fmt.Sprintf(
		"%s/%s",
		data.DefaultAdminCertDir,
		data.DefaultAdminPublicCertFileName,
	)
	privateCertPath := fmt.Sprintf(
		"%s/%s",
		data.DefaultAdminCertDir,
		data.DefaultAdminPrivateCertFileName,
	)
	return &Config{
		acme: ACME{
			Email: DefaultACMEEmail,
		},
		tlsHost:     tlsHost,
		tlsAuto:     tlsAuto,
		tlsCertPath: publicCertPath,
		tlsKeyPath:  privateCertPath,
		adminNetAddress: net.TCPAddr{
			IP:   net.IPv4(0, 0, 0, 0),
			Port: DefaultProductionAdministrationPort,
		},
		phishingHTTPNetAddress: net.TCPAddr{
			IP:   net.IPv4(0, 0, 0, 0),
			Port: DefaultProductionHTTPPhishingPort,
		},
		phishingHTTPSNetAddress: net.TCPAddr{
			IP:   net.IPv4(0, 0, 0, 0),
			Port: DefaultProductionHTTPSPhishingPort,
		},

		database: Database{
			Engine: DefaultAdministrationUseSqlite,
			DSN:    DefaultAdministrationDSN,
		},
		fileWriter: &file.FileWriter{},
		IPSecurity: IPSecurityConfig{
			AdminAllowed:    []string{},
			TrustedProxies:  []string{},
			TrustedIPHeader: "",
		},
	}
}

// ACMEEmail returns the acme email
func (c *Config) ACMEEmail() string {
	return c.acme.Email
}

// SetACMEEmail sets the acme email
func (c *Config) SetACMEEmail(email string) {
	c.acme.Email = email
}

// TLSHost returns the host to use for admin server
func (c *Config) TLSHost() string {
	return c.tlsHost
}

// TLSAuto returns if ACME service should handle TLS for the admin server
func (c *Config) TLSAuto() bool {
	return c.tlsAuto
}

// TLSCertPath returns the cert path
func (c *Config) TLSCertPath() string {
	return c.tlsCertPath
}

// TLSKeyPath returns the private key
func (c *Config) TLSKeyPath() string {
	return c.tlsKeyPath
}

// SetTLSHost sets the admin host
func (c *Config) SetTLSHost(host string) {
	c.tlsHost = host
}

// SetTLSCertPath sets the TLS certificate path
func (c *Config) SetTLSCertPath(certPath string) {
	c.tlsCertPath = certPath
}

// SetTLSKeyPath sets the TLS private key path
func (c *Config) SetTLSKeyPath(keyPath string) {
	c.tlsKeyPath = keyPath
}

// SetTLSAuto sets if a ACME service should handle TLS for the admin server
func (c *Config) SetTLSAuto(auto bool) {
	c.tlsAuto = auto
}

// SetAdminNetAddress sets the administration network address
func (c *Config) SetAdminNetAddress(adminNetAddress string) error {
	newAddr, err := StringAddressToTCPAddr(adminNetAddress)
	if err != nil {
		return err
	}
	c.adminNetAddress = *newAddr
	return nil
}

// SetPhishingHTTPNetAddress sets the phishing network address
func (c *Config) SetPhishingHTTPNetAddress(addr string) error {
	newAddr, err := StringAddressToTCPAddr(addr)
	if err != nil {
		return err
	}
	c.phishingHTTPNetAddress = *newAddr
	return nil
}

// SetPhishingHTTPNetAddress sets the phishing network address
func (c *Config) SetPhishingHTTPSNetAddress(addr string) error {
	newAddr, err := StringAddressToTCPAddr(addr)
	if err != nil {
		return err
	}
	c.phishingHTTPSNetAddress = *newAddr
	return nil
}

// SetDatabaseEngine sets the database engine
func (c *Config) SetDatabaseEngine(engine string) {
	c.database.Engine = engine
}

// SetDatabaseDSN sets the database DSN
func (c *Config) SetDatabaseDSN(dsn string) {
	c.database.DSN = dsn
}

// SetLogPath sets the log file path
func (c *Config) SetLogPath(path string) {
	c.LogPath = path
}

// SetErrLogPath sets the error log file path
func (c *Config) SetErrLogPath(path string) {
	c.ErrLogPath = path
}

// SetRemoteBrowserEnabled enables or disables the remote browser feature.
func (c *Config) SetRemoteBrowserEnabled(enabled bool) {
	c.RemoteBrowser.Enabled = enabled
}

// SetRemoteBrowserExecPath sets the Chrome binary path used by the remote browser runner.
func (c *Config) SetRemoteBrowserExecPath(path string) {
	c.RemoteBrowser.ExecPath = path
}

// SetFileWriter sets the file writer
func (c *Config) SetFileWriter(fileWriter file.Writer) error {
	if err := ValidateFileWriter(fileWriter); err != nil {
		return fmt.Errorf("failed to set file writer on config: %w", err)
	}
	c.fileWriter = fileWriter
	return nil
}

// Write writes the config to a writer
func (c *Config) WriteToFile(filepath string) error {
	dto := c.ToDTO()
	conf, err := json.MarshalIndent(dto, "", "  ")
	if err != nil {
		return fmt.Errorf("failed to marshal config: %w", err)
	}

	// Write the content to the writer
	if _, err := c.fileWriter.Write(filepath, conf, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644); err != nil {
		return fmt.Errorf("failed to write config: %w", err)
	}

	return nil
}

// StringAddressToTCPAddr converts a string address to a TCPAddr
func StringAddressToTCPAddr(address string) (*net.TCPAddr, error) {
	host, port, err := net.SplitHostPort(address)
	if err != nil {
		return nil, errs.Wrap(err)
	}
	ip := net.ParseIP(host)
	if ip == nil {
		return nil, ErrInvalidIP
	}
	// convert port to int
	p, err := strconv.Atoi(port)
	if err != nil {
		return nil, errs.Wrap(err)
	}
	if p < 0 || p > 65535 {
		return nil, ErrInvalidPort
	}
	return &net.TCPAddr{
		IP:   ip,
		Port: p,
	}, nil
}

// FromMap creates a *Config from a DTO
func FromDTO(dto *ConfigDTO) (*Config, error) {
	cfg, err := NewConfig(
		dto.ACME.Email,
		dto.AdministrationServer.TLSHost,
		dto.AdministrationServer.TLSAuto,
		dto.AdministrationServer.TLSCertPath,
		dto.AdministrationServer.TLSKeyPath,
		dto.AdministrationServer.Address,
		dto.PhishingServer.Http,
		dto.PhishingServer.Https,
		dto.Database,
		file.FileWriter{},
		dto.Log.Path,
		dto.Log.ErrorPath,
		dto.IPSecurity,
	)
	if err != nil {
		return nil, err
	}
	cfg.RemoteBrowser = dto.RemoteBrowser
	cfg.Authentication = dto.Authentication
	return cfg, nil
}

// ToDTO converts a *Config to a *ConfigDTO
func (c *Config) ToDTO() *ConfigDTO {
	return &ConfigDTO{
		ACME: ACME{
			Email: c.acme.Email,
		},
		AdministrationServer: AdministrationServer{
			TLSHost:     c.TLSHost(),
			TLSAuto:     c.TLSAuto(),
			TLSCertPath: c.TLSCertPath(),
			TLSKeyPath:  c.TLSKeyPath(),
			Address:     c.AdminNetAddress(),
		},
		PhishingServer: PhishingServer{
			Http:  c.phishingHTTPNetAddress.String(),
			Https: c.phishingHTTPSNetAddress.String(),
		},
		Database: Database{
			Engine: c.database.Engine,
			DSN:    c.database.DSN,
		},
		Log: Log{
			Path:      c.LogPath,
			ErrorPath: c.ErrLogPath,
		},
		IPSecurity:     c.IPSecurity,
		RemoteBrowser:  c.RemoteBrowser,
		Authentication: c.Authentication,
	}
}

// AdminNetAddress returns the administration network address
func (c *Config) AdminNetAddress() string {
	return c.adminNetAddress.String()
}

// AdminNetAddressPort returns the administration network address port
func (c *Config) AdminNetAddressPort() int {
	return c.adminNetAddress.Port
}

// PhishingHTTPNetAddress returns the phishing network address
func (c *Config) PhishingHTTPNetAddress() string {
	return c.phishingHTTPNetAddress.String()
}

// PhishingHTTPNetAddressPort returns the phishing network address port
func (c *Config) PhishingHTTPNetAddressPort() int {
	return c.phishingHTTPNetAddress.Port
}

// PhishingHTTPSNetAddress returns the phishing network address
func (c *Config) PhishingHTTPSNetAddress() string {
	return c.phishingHTTPSNetAddress.String()
}

// PhishingHTTPSNetAddressPort returns the phishing network address port
func (c *Config) PhishingHTTPSNetAddressPort() int {
	return c.phishingHTTPSNetAddress.Port
}

// Database returns the database
func (c *Config) Database() Database {
	return c.database
}

// NewDTOFromFile creates a *ConfigDTO from a file
func NewDTOFromFile(filesystem fs.FS, path string) (*ConfigDTO, error) {
	var conf ConfigDTO
	f, err := filesystem.Open(path)
	if err != nil {
		return nil, errs.Wrap(err)
	}
	dec := json.NewDecoder(f)
	err = dec.Decode(&conf)
	if err != nil {
		return nil, errs.Wrap(err)
	}
	return &conf, nil
}
