package cfg

import (
	"fmt"
	"log/syslog"
	"net/url"
	"os"
	"path"
	"strings"
	"time"

	"code.justin.tv/infosec/cors"

	"github.com/Sirupsen/logrus"
	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/aws/credentials"
	"github.com/aws/aws-sdk-go/aws/credentials/stscreds"
	awssession "github.com/aws/aws-sdk-go/aws/session"
	"github.com/aws/aws-sdk-go/service/sts"
	"github.com/bsdlp/config"
	"github.com/imdario/mergo"
)

// configuration defaults
const (
	DefaultLDAPAddress             = "ldap-vip.internal.justin.tv"
	DefaultLDAPPort                = 636
	DefaultLDAPUser                = "guardian"
	DefaultTestClientsTable        = "guardian-clients-test"
	DefaultTestAuthorizationsTable = "guardian-authorizations-test"
	DefaultLogPrefix               = "guardian"
	DefaultClientsID               = "c54dca37-1dda-4826-9eee-339bb0b3b777"
	DefaultDevBaseURL              = "http://localhost:8181"
	DefaultProdBaseURL             = "https://guardian.prod.us-west2.justin.tv/"
	DefaultDebug                   = false
	DefaultAWSRegion               = "us-west-2"
	StagingLDAPAddress             = "ldap.dev.us-west2.justin.tv"
	StagingLDAPPort                = 636
	StagingLDAPUser                = "Guardian User"
	StagingLDAPPassword            = "test_password"
	DefaultCORSMaxAge              = "5h"
	DefaultReportRate              = 5 * time.Second
)

// Config holds config options for guardian
type Config struct {
	LDAP    *LDAPConfig    `yaml:"ldap"`
	DB      *DBConfig      `yaml:"db"`
	Logging *LoggingConfig `yaml:"logging"`
	Admin   *AdminConfig   `yaml:"admin"`
	Assets  *AssetConfig   `yaml:"assets"`
	CORS    *CORSConfig    `yaml:"cors"`
	Metrics MetricsConfig  `yaml:"metrics"`
	Debug   bool           `yaml:"debug"`
}

// GetLogger returns logger based on config options
func (c *Config) GetLogger() (logger *logrus.Logger, err error) {
	logger = logrus.New()
	logger.Out = os.Stderr

	logger.Level, err = logrus.ParseLevel(c.Logging.Level)
	if err != nil {
		err = fmt.Errorf("error parsing logging level: %s", err.Error())
		return
	}

	if c.Logging.Syslog {
		logger.Out, err = syslog.New(syslog.LOG_LOCAL4, DefaultLogPrefix)
		if err != nil {
			err = fmt.Errorf("error setting up syslog: %s", err.Error())
			return
		}
		logger.Formatter = new(logrus.JSONFormatter)
	}
	return
}

// MetricsConfig holds config options for metrics
type MetricsConfig struct {
	GraphiteHostPort string        `yaml:"graphite_host_port"`
	ReportRate       time.Duration `yaml:"report_rate"`
}

// AdminConfig holds config options for clients admin api
type AdminConfig struct {
	// ClientsID is the oauth2 client id for the clients admin api. Defaults to
	// c54dca37-1dda-4826-9eee-339bb0b3b777.
	ClientsID string `yaml:"clients_id"`
	// BaseURL is the base url for the admin endpoint. Defaults to
	// http://localhost:8181/.
	BaseURL string `yaml:"base_url"`
}

// RedirectURI constructs redirect uri for the clients admin api based on config
// values and provided relpath ("/auth/callback")
func (ac *AdminConfig) RedirectURI(relpath string) (uri string, err error) {
	rURI, err := url.Parse(ac.BaseURL)
	if err != nil {
		err = fmt.Errorf("error parsing configured base_url: %s", err.Error())
		return
	}
	rURI.Path = path.Join(rURI.Path, relpath)
	uri = rURI.String()
	return
}

// LoggingConfig holds config options for logging
type LoggingConfig struct {
	Syslog bool   `yaml:"syslog"`
	Level  string `yaml:"level"`
}

// LDAPConfig holds config options for ldap
type LDAPConfig struct {
	UseTLS       bool   `yaml:"use_tls"`
	Address      string `yaml:"address"`
	Port         int    `yaml:"port"`
	BindUser     string `yaml:"bind_user"`
	BindPassword string `yaml:"bind_password"`
}

// DBConfig holds dynamodb config options
type DBConfig struct {
	ClientsTable        string `yaml:"clients_table"`
	AuthorizationsTable string `yaml:"authorizations_table"`
	AWSRegion           string `yaml:"aws_region"`
	AWSEndpoint         string `yaml:"aws_endpoint"`
	AWSConfig           *aws.Config
	AWSSession          *awssession.Session
	RoleArn             string `yaml:"role_arn"`
}

// CORSConfig holds cors config options
type CORSConfig struct {
	AllowedOrigins []cors.OriginList `yaml:"allowed_origins"`
	// duration, in a format parseable by https://golang.org/pkg/time/#ParseDuration
	MaxAge string `yaml:"max_age"`
	maxAge time.Duration
}

// SetMaxAge parses max age and sets CORSConfig.maxAge
func (c *CORSConfig) SetMaxAge() (err error) {
	duration, err := time.ParseDuration(c.MaxAge)
	if err != nil {
		return
	}
	c.maxAge = duration
	return
}

// Policy returns a cors.Policy
func (c *CORSConfig) Policy() cors.Policy {
	return cors.Policy{
		MaxAge:         cors.MaxAge(c.maxAge),
		AllowedOrigins: cors.Origins(c.AllowedOrigins...),
	}
}

// AssetConfig manages asset paths and settings
type AssetConfig struct {
	AssetPath string `yaml:"asset_path"`
}

// BuildDefaultAssetPath returns an absolute path to the expect public asset folder
func BuildDefaultAssetPath() string {
	// this is specifically for making Godeps magic paths play nicely
	gopath := strings.Split(os.Getenv("GOPATH"), "src")[0]

	return path.Join(gopath, "/src/code.justin.tv/systems/guardian")
}

// DefaultConfig returns a default config object
func DefaultConfig() *Config {
	return &Config{
		LDAP: &LDAPConfig{
			UseTLS:   true,
			Address:  DefaultLDAPAddress,
			Port:     DefaultLDAPPort,
			BindUser: DefaultLDAPUser,
		},
		DB: &DBConfig{
			ClientsTable:        DefaultTestClientsTable,
			AuthorizationsTable: DefaultTestAuthorizationsTable,
			AWSRegion:           DefaultAWSRegion,
			AWSConfig:           aws.NewConfig().WithRegion(DefaultAWSRegion),
		},
		Logging: &LoggingConfig{
			Syslog: false,
			Level:  "info",
		},
		Admin: &AdminConfig{
			ClientsID: DefaultClientsID,
			BaseURL:   DefaultDevBaseURL,
		},
		Assets: &AssetConfig{
			AssetPath: BuildDefaultAssetPath(),
		},
		CORS: &CORSConfig{
			AllowedOrigins: []cors.OriginList{
				"http://localhost",
				"https://localhost",
				"https://dashboard.internal.justin.tv",
				DefaultProdBaseURL,
			},
			MaxAge: DefaultCORSMaxAge,
		},
		Debug: DefaultDebug,
	}
}

// UseStagingLDAP changes LDAP properties to represent staging environment
func (c *Config) UseStagingLDAP() {
	c.LDAP.Port = StagingLDAPPort
	c.LDAP.Address = StagingLDAPAddress
	c.LDAP.BindUser = StagingLDAPUser
	c.LDAP.BindPassword = StagingLDAPPassword
}

// LoadConfig reads config file from either user or system config
// paths:
// * $HOME/.config/twitch/guardian/config.yaml
// * /etc/twitch/guardian/config.yaml
func LoadConfig() (c *Config, err error) {
	c = &Config{}

	err = config.Namespace{
		Organization: "twitch",
		System:       "guardian",
	}.Load(c)
	if err != nil {
		if err == config.ErrConfigFileNotFound {
			err = nil
		} else {
			return
		}
	}

	err = mergo.Merge(c, DefaultConfig())
	if err != nil {
		return
	}

	// check if we can parse cors.MaxAge
	err = c.CORS.SetMaxAge()
	if err != nil {
		return
	}

	c.DB.AWSConfig = aws.NewConfig().WithRegion(c.DB.AWSRegion)

	if c.DB.AWSEndpoint != "" {
		c.DB.AWSConfig.WithEndpoint(c.DB.AWSEndpoint)
	}

	if c.DB.RoleArn != "" {
		stsclient := sts.New(awssession.New(c.DB.AWSConfig))
		arp := &stscreds.AssumeRoleProvider{
			Duration:     900 * time.Second,
			ExpiryWindow: 10 * time.Second,
			RoleARN:      c.DB.RoleArn,
			Client:       stsclient,
		}
		creds := credentials.NewCredentials(arp)
		c.DB.AWSConfig.WithCredentials(creds)
	}
	c.DB.AWSSession = awssession.New(c.DB.AWSConfig)
	return
}
