package main

import (
	"fmt"
	"log"
	"net"
	"os"
	"strconv"
	"time"

	"github.com/afex/hystrix-go/hystrix"
	"github.com/afex/hystrix-go/hystrix/metric_collector"
	"github.com/afex/hystrix-go/plugins"
	"github.com/cactus/go-statsd-client/statsd"

	"code.justin.tv/chat/redicache"
	"code.justin.tv/chat/redis"
	"code.justin.tv/common/config"
	"code.justin.tv/common/twitchhttp"
	"code.justin.tv/identity/connections/app/api"
	"code.justin.tv/identity/connections/app/backend"
	"code.justin.tv/identity/connections/app/clients"
	"code.justin.tv/identity/connections/app/clients/postgres"
	"code.justin.tv/identity/connections/app/clients/steam"
	"code.justin.tv/identity/connections/app/clients/twitter"
	"code.justin.tv/identity/connections/app/clients/youtube"
	"code.justin.tv/identity/connections/app/utils/auth"
	"code.justin.tv/identity/connections/app/utils/cryptor"
)

func main() {
	// Registered config holds default values that will be overwritten
	// by environment variables. Elastic Beanstalk "Software Configuration"
	// will set environment variables to overwrite these defaults.
	config.Register(map[string]string{
		// App Config
		// These config values are set by jtv/common/config. Do not uncomment.
		//
		// "app":              "connections",
		// "environment":      "development",
		// "statsd-host-port": "statsd.internal.justin.tv:8125",
		// "rollbar-token":    "",

		// Postgres Config
		// Defaults are set to connect to staging sitedb.
		//
		"PG_HOST_MASTER":          "master-sitedb.staging.us-west2.justin.tv",
		"PG_PORT_MASTER":          "6542",
		"PG_HOST_SLAVE":           "master-sitedb.staging.us-west2.justin.tv",
		"PG_PORT_SLAVE":           "6542",
		"PG_USER":                 "change_me",
		"PG_PASSWORD":             "change_me",
		"PG_DATABASE":             "site_justintv_dev",
		"PG_DRIVER_NAME":          "postgres",
		"PG_MAX_OPEN_CONNS":       "500",
		"PG_MAX_IDLE_CONNS":       "250",
		"PG_MAX_QUEUE_SIZE":       "250",
		"PG_CONN_ACQUIRE_TIMEOUT": "1",
		"PG_REQUEST_TIMEOUT":      "2",
		"PG_MAX_CONN_AGE":         "120",

		// RDS Config
		//
		"RDS_HOST_MASTER":          "connections-dev.cekokguegnp4.us-west-2.rds.amazonaws.com",
		"RDS_PORT_MASTER":          "5432",
		"RDS_HOST_SLAVE":           "connections-dev.cekokguegnp4.us-west-2.rds.amazonaws.com",
		"RDS_PORT_SLAVE":           "5432",
		"RDS_DATABASE":             "connections",
		"RDS_USER":                 "connections",
		"RDS_PASS":                 "teamsalsaverde",
		"RDS_DRIVER_NAME":          "postgres",
		"RDS_MAX_OPEN_CONNS":       "500",
		"RDS_MAX_IDLE_CONNS":       "250",
		"RDS_MAX_QUEUE_SIZE":       "250",
		"RDS_CONN_ACQUIRE_TIMEOUT": "1",
		"RDS_REQUEST_TIMEOUT":      "2",
		"RDS_MAX_CONN_AGE":         "120",

		// Twitter Config
		//
		"TWITTER_CONSUMER_KEY":    "change_me",
		"TWITTER_CONSUMER_SECRET": "change_me",
		"TWITTER_ENCRYPTION_KEY":  "This Should Be 32Bytes In Length",
		"TWITTER_CALLBACK_URL":    "https://api.twitch.tv/v4/twitter/callback?id=%s&client_id=%s&nonce=%s",

		// Steam Config
		//
		"STEAM_CALLBACK_URL": "http://localhost.twitch.tv:8000/v1/%s/platforms/steam/callback?client_id=%s",
		"STEAM_ROOT_URL":     "http://localhost.twitch.tv:8000",

		// Youtube Config
		"YOUTUBE_CLIENT_ID":     "994605931410-73thh6q8u20ucqn5c5latalk13mf6opu.apps.googleusercontent.com",
		"YOUTUBE_CLIENT_SECRET": "5N_2eerfphFI4W1MTnqS1csm",
		"YOUTUBE_CALLBACK_URL":  "http://api.twitch.tv/v5/youtube/callback",

		// Redis Config
		//
		"REDIS_HOST":            "localhost",
		"REDIS_PORT":            "6379",
		"REDIS_PASSWORD":        "",
		"REDIS_CONNECT_TIMEOUT": "500",
		"REDIS_READ_TIMEOUT":    "500",
		"REDIS_WRITE_TIMEOUT":   "100",
		"REDIS_MAX_CONNECTIONS": "1000",

		// Owl Config
		//
		"OWL_HOST": "change_me",
		"OWL_PORT": "change_me",

		// Cartman Config
		//
		"CARTMAN_HMAC_KEY": "nerf this!",
	})

	err := config.Parse()
	if err != nil {
		log.Fatal(err)
	}

	stats := config.Statsd()
	redisClient := initRedis(stats)
	rediCache := initRedicache(redisClient)
	backender := initBackend(stats, rediCache)
	twitterClient := initTwitter(redisClient)
	steamClient := initSteam(redisClient)
	youtubeClient := initYoutube(redisClient)
	initHystrix()

	authenticator := initAuthenticator()
	server, err := api.NewServer(backender, twitterClient, steamClient, youtubeClient, authenticator, stats)
	if err != nil {
		log.Fatal(err)
	}
	backend.Cryptors = initCryptors()

	log.Fatal(twitchhttp.ListenAndServe(server))
}

func initRedis(statter statsd.Statter) redis.Redis {
	conf := redicache.Config{
		Host:           config.Resolve("REDIS_HOST"),
		Port:           configResolveInt("REDIS_PORT"),
		Password:       config.Resolve("REDIS_PASSWORD"),
		KeyPrefix:      fmt.Sprintf("connections:%s", config.Resolve("environment")),
		StatsPrefix:    "redis",
		ConnectTimeout: configResolveDuration("REDIS_CONNECT_TIMEOUT"),
		ReadTimeout:    configResolveDuration("REDIS_READ_TIMEOUT"),
		WriteTimeout:   configResolveDuration("REDIS_WRITE_TIMEOUT"),
		MaxConns:       configResolveInt("REDIS_MAX_CONNECTIONS"),
	}

	cache, err := redis.Init{
		Address:   net.JoinHostPort(conf.Host, strconv.Itoa(conf.Port)),
		Password:  conf.Password,
		KeyPrefix: conf.KeyPrefix,

		ConnectTimeout: time.Duration(conf.ConnectTimeout),
		ReadTimeout:    time.Duration(conf.ReadTimeout),
		WriteTimeout:   time.Duration(conf.WriteTimeout),
		MaxConns:       conf.MaxConns,

		XactGroup:   "redis",
		StatsPrefix: conf.StatsPrefix,
		Stats:       statter,
	}.New()

	if err != nil {
		log.Fatalf("failed to initialize redis: %s", err.Error())
	}

	return cache
}

func initRedicache(r redis.Redis) redicache.Cache {
	return redicache.NewFromRedis(r)
}

// Configure SiteDB + Redis Backend
func initBackend(statter statsd.Statter, cache redicache.Cache) backend.ConnectionsBackender {
	pconfMaster := postgres.Config{
		Host:               config.Resolve("PG_HOST_MASTER"),
		Port:               configResolveInt("PG_PORT_MASTER"),
		User:               config.Resolve("PG_USER"),
		Password:           config.Resolve("PG_PASSWORD"),
		Database:           config.Resolve("PG_DATABASE"),
		DriverName:         config.Resolve("PG_DRIVER_NAME"),
		MaxOpenConns:       configResolveInt("PG_MAX_OPEN_CONNS"),
		MaxIdleConns:       configResolveInt("PG_MAX_IDLE_CONNS"),
		MaxQueueSize:       configResolveInt("PG_MAX_QUEUE_SIZE"),
		ConnAcquireTimeout: configResolveInt("PG_CONN_ACQUIRE_TIMEOUT"),
		RequestTimeout:     configResolveInt("PG_REQUEST_TIMEOUT"),
		MaxConnAge:         configResolveInt("PG_MAX_CONN_AGE"),
	}
	pgDbMaster, err := postgres.New(pconfMaster, statter)
	if err != nil {
		log.Fatalf("failed to initialize pg backend: %s", err.Error())
	}

	pconfSlave := postgres.Config{
		Host:               config.Resolve("PG_HOST_SLAVE"),
		Port:               configResolveInt("PG_PORT_SLAVE"),
		User:               config.Resolve("PG_USER"),
		Password:           config.Resolve("PG_PASSWORD"),
		Database:           config.Resolve("PG_DATABASE"),
		DriverName:         config.Resolve("PG_DRIVER_NAME"),
		MaxOpenConns:       configResolveInt("PG_MAX_OPEN_CONNS"),
		MaxIdleConns:       configResolveInt("PG_MAX_IDLE_CONNS"),
		MaxQueueSize:       configResolveInt("PG_MAX_QUEUE_SIZE"),
		ConnAcquireTimeout: configResolveInt("PG_CONN_ACQUIRE_TIMEOUT"),
		RequestTimeout:     configResolveInt("PG_REQUEST_TIMEOUT"),
		MaxConnAge:         configResolveInt("PG_MAX_CONN_AGE"),
	}
	pgDbSlave, err := postgres.New(pconfSlave, statter)
	if err != nil {
		log.Fatalf("failed to initialize pg backend: %s", err.Error())
	}

	rconfMaster := postgres.Config{
		Host:               config.Resolve("RDS_HOST_MASTER"),
		Port:               configResolveInt("RDS_PORT_MASTER"),
		User:               config.Resolve("RDS_USER"),
		Password:           config.Resolve("RDS_PASS"),
		Database:           config.Resolve("RDS_DATABASE"),
		DriverName:         config.Resolve("RDS_DRIVER_NAME"),
		MaxOpenConns:       configResolveInt("RDS_MAX_OPEN_CONNS"),
		MaxIdleConns:       configResolveInt("RDS_MAX_IDLE_CONNS"),
		MaxQueueSize:       configResolveInt("RDS_MAX_QUEUE_SIZE"),
		ConnAcquireTimeout: configResolveInt("RDS_CONN_ACQUIRE_TIMEOUT"),
		RequestTimeout:     configResolveInt("RDS_REQUEST_TIMEOUT"),
		MaxConnAge:         configResolveInt("RDS_MAX_CONN_AGE"),
	}

	rdsDbMaster, err := postgres.New(rconfMaster, statter)
	if err != nil {
		log.Fatalf("failed to initialize rds backend: %s", err.Error())
	}

	rconfSlave := postgres.Config{
		Host:               config.Resolve("RDS_HOST_SLAVE"),
		Port:               configResolveInt("RDS_PORT_SLAVE"),
		User:               config.Resolve("RDS_USER"),
		Password:           config.Resolve("RDS_PASS"),
		Database:           config.Resolve("RDS_DATABASE"),
		DriverName:         config.Resolve("RDS_DRIVER_NAME"),
		MaxOpenConns:       configResolveInt("RDS_MAX_OPEN_CONNS"),
		MaxIdleConns:       configResolveInt("RDS_MAX_IDLE_CONNS"),
		MaxQueueSize:       configResolveInt("RDS_MAX_QUEUE_SIZE"),
		ConnAcquireTimeout: configResolveInt("RDS_CONN_ACQUIRE_TIMEOUT"),
		RequestTimeout:     configResolveInt("RDS_REQUEST_TIMEOUT"),
		MaxConnAge:         configResolveInt("RDS_MAX_CONN_AGE"),
	}

	rdsDbSlave, err := postgres.New(rconfSlave, statter)
	if err != nil {
		log.Fatalf("failed to initialize rds backend: %s", err.Error())
	}

	return backend.NewPGBackend(pgDbMaster, pgDbSlave, rdsDbMaster, rdsDbSlave, statter, cache)
}

func initCryptors() cryptor.MultiCryptor {
	params := map[string]string{
		backend.TwitterPlatform: config.Resolve("TWITTER_ENCRYPTION_KEY"),
	}
	multi, err := cryptor.NewMultiCryptor(params)
	if err != nil {
		log.Fatalf("failed to initialize multicryptor: %q", err)
	}

	return multi
}

func initAuthenticator() auth.Authenticator {
	auther, err := auth.NewAuthenticator(config.Resolve("CARTMAN_HMAC_KEY"))
	if err != nil {
		log.Fatalf("failed to initialize authenticator: %q", err)
	}

	return auther
}

func initTwitter(r redis.Redis) clients.Client {
	conf := twitter.Config{
		CallbackURL:    config.Resolve("TWITTER_CALLBACK_URL"),
		ConsumerKey:    config.Resolve("TWITTER_CONSUMER_KEY"),
		ConsumerSecret: config.Resolve("TWITTER_CONSUMER_SECRET"),
	}
	client, err := twitter.NewClient(conf, r)
	if err != nil {
		log.Fatalf("failed to initialize twitter client: %s", err.Error())
	}
	return client
}

func initSteam(r redis.Redis) clients.Client {
	client, err := steam.NewClient(
		config.Resolve("STEAM_CALLBACK_URL"),
		config.Resolve("STEAM_ROOT_URL"),
		r,
	)
	if err != nil {
		log.Fatalf("failed to initialize steam client: %s", err.Error())
	}
	return client
}

func initYoutube(r redis.Redis) clients.Client {
	client, err := youtube.NewClient(
		youtube.Config{
			ClientID:     config.Resolve("YOUTUBE_CLIENT_ID"),
			ClientSecret: config.Resolve("YOUTUBE_CLIENT_SECRET"),
			RedirectURL:  config.Resolve("YOUTUBE_CALLBACK_URL"),
		},
		r,
	)
	if err != nil {
		log.Fatalf("failed to initialize youtube client: %s", err.Error())
	}
	return client
}

func initHystrix() {
	hystrix.Configure(map[string]hystrix.CommandConfig{
		// The redis command is given a larger timeout and max pool of concurrent
		// requests due to it calling an encapsulated hystrix command for "sitedb".
		"redis": hystrix.CommandConfig{
			Timeout:                2000,
			MaxConcurrentRequests:  1000,
			RequestVolumeThreshold: 20,
			SleepWindow:            5000,
			ErrorPercentThreshold:  50,
		},
		"sitedb_query": hystrix.CommandConfig{
			Timeout:                500,
			MaxConcurrentRequests:  300,
			RequestVolumeThreshold: 20,
			SleepWindow:            5000,
			ErrorPercentThreshold:  50,
		},
		"sitedb_exec": hystrix.CommandConfig{
			Timeout:                500,
			MaxConcurrentRequests:  300,
			RequestVolumeThreshold: 20,
			SleepWindow:            5000,
			ErrorPercentThreshold:  50,
		},
		"rds_query": hystrix.CommandConfig{
			Timeout:                300,
			MaxConcurrentRequests:  300,
			RequestVolumeThreshold: 20,
			SleepWindow:            5000,
			ErrorPercentThreshold:  50,
		},
		"rds_exec": hystrix.CommandConfig{
			Timeout:                300,
			MaxConcurrentRequests:  300,
			RequestVolumeThreshold: 20,
			SleepWindow:            5000,
			ErrorPercentThreshold:  50,
		},
		"getTwitterAccessToken": hystrix.CommandConfig{
			Timeout:                500,
			MaxConcurrentRequests:  100,
			RequestVolumeThreshold: 20,
			SleepWindow:            5000,
			ErrorPercentThreshold:  50,
		},
		"exchangeYoutubeCode": hystrix.CommandConfig{
			Timeout:                2000,
			MaxConcurrentRequests:  100,
			RequestVolumeThreshold: 20,
			SleepWindow:            5000,
			ErrorPercentThreshold:  50,
		},
		"getYoutubeID": hystrix.CommandConfig{
			Timeout:                2000,
			MaxConcurrentRequests:  100,
			RequestVolumeThreshold: 20,
			SleepWindow:            5000,
			ErrorPercentThreshold:  50,
		},
		"refreshYoutubeToken": hystrix.CommandConfig{
			Timeout:                2000,
			MaxConcurrentRequests:  100,
			RequestVolumeThreshold: 20,
			SleepWindow:            5000,
			ErrorPercentThreshold:  50,
		},
	})

	host, err := os.Hostname()
	if err != nil {
		host = "unknown"
	}
	c, err := plugins.InitializeStatsdCollector(&plugins.StatsdCollectorConfig{
		StatsdAddr: config.StatsdHostPort(),
		Prefix:     fmt.Sprintf("%s.%s.%s.hystrix", "connections", config.Environment(), host),
	})
	if err != nil {
		log.Fatalf("failed to initailize hystrix statsd client: %s", err)
		return
	}
	metricCollector.Registry.Register(c.NewStatsdCollector)
}

func configResolveInt(field string) int {
	i, err := strconv.Atoi(config.Resolve(field))
	if err != nil {
		log.Fatal(fmt.Sprintf("config parameter '%s' is not valid: ", field), err)
	}
	return i
}

func configResolveDuration(field string) time.Duration {
	i := configResolveInt(field)
	return time.Duration(i) * time.Millisecond
}
