package main

import (
	"errors"
	"fmt"
	"io/ioutil"
	"log"
	"net"
	"net/http"
	"net/url"
	"strconv"
	"strings"
	"time"

	_ "github.com/lib/pq"

	"code.justin.tv/chat/db"
	"code.justin.tv/feeds/following-service/client/follows"

	"code.justin.tv/chat/redicache"
	"code.justin.tv/chat/redis"
	"code.justin.tv/common/config"
	"code.justin.tv/common/spade-client-go/spade"
	"code.justin.tv/common/twitchhttp"
	"code.justin.tv/web/blacklist"
	partnerships "code.justin.tv/web/partnerships/client"
	"code.justin.tv/web/users-service/api"
	"code.justin.tv/web/users-service/backend/user"
	"code.justin.tv/web/users-service/database"
	"code.justin.tv/web/users-service/internal/clients/banreport"
	"code.justin.tv/web/users-service/internal/clients/cache"
	"code.justin.tv/web/users-service/internal/clients/hystrix"
	"code.justin.tv/web/users-service/internal/clients/kinesis"
	"code.justin.tv/web/users-service/logic"
)

func main() {
	config.Register(map[string]string{
		"db-host-slave":                    "localhost",
		"db-port-slave":                    "5432",
		"db-max-open-conns-slave":          "100",
		"db-max-idle-conns-slave":          "50",
		"db-max-queue-size-slave":          "10",
		"db-conn-acquire-timeout-ms-slave": "1000",
		"db-request-timeout-ms-slave":      "5000",
		"db-max-conn-age-ms-slave":         "60000",

		"db-host-master":                    "localhost",
		"db-port-master":                    "5432",
		"db-max-open-conns-master":          "100",
		"db-max-idle-conns-master":          "50",
		"db-max-queue-size-master":          "10",
		"db-conn-acquire-timeout-ms-master": "1000",
		"db-request-timeout-ms-master":      "5000",
		"db-max-conn-age-ms-master":         "60000",

		"db-name":          "users_service_justintv_dev",
		"db-user":          "users_service_01",
		"db-password":      "",
		"db-password-file": ".dbpass_dev",

		"rails-host": "127.0.0.1:3000",

		"redis-host":                   "localhost",
		"redis-port":                   "14912",
		"redis-pass":                   "",
		"redis-connect-timeout-ms":     "500",
		"redis-read-timeout-ms":        "500",
		"redis-write-timeout-ms":       "100",
		"redis-conn":                   "1000",
		"redis-user-properties-ttl-ms": "30000",

		"follows-host": "internal-following-service-staging-app-2022864404.us-west-2.elb.amazonaws.com",

		"user-mutations-stream-role":           "arn:aws:iam::465369119046:role/user-mutations-stream-role",
		"user-mutations-stream-name":           "user-mutations-stream",
		"user-mutations-stream-region":         "us-west-2",
		"user-mutations-stream-retry-count":    "10",
		"user-mutations-stream-retry-delay-ms": "100",
		"partnerships-host":                    "partnerships.dev.us-west2.twitch.tv",
		"spade-host":                           "spade.internal.justin.tv",
	})

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

	ub, err := configureUserBackend()
	if err != nil {
		log.Fatal(err)
	}

	brc, err := configureBanReportClient()
	if err != nil {
		log.Fatal(err)
	}

	bc, err := configureBlacklistClient()
	if err != nil {
		log.Fatal(err)
	}

	cc, err := configureCacheClient()
	if err != nil {
		log.Fatal(err)
	}

	follows, err := configureFollowsClient()
	if err != nil {
		log.Fatal(err)
		return
	}

	kc, err := configureKinesisPublisher()
	if err != nil {
		log.Fatal(err)
		return
	}

	partners, err := configurePartnershipsClient()
	if err != nil {
		log.Fatal(err)
		return
	}

	spade, err := configureSpadeClient()
	if err != nil {
		log.Fatal(err)
		return
	}

	hystrix.InitHystrix()

	l, err := logic.New(ub, brc, bc, cc, follows, kc, partners, spade)
	if err != nil {
		log.Fatal(err)
	}

	server, err := api.NewServer(l)
	if err != nil {
		log.Fatal(err)
	}

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

func configureFollowsClient() (follows.Client, error) {
	return follows.NewClient(twitchhttp.ClientConf{
		Host:  config.Resolve("follows-host"),
		Stats: config.Statsd(),
	})
}

func configurePartnershipsClient() (partnerships.Client, error) {
	return partnerships.NewClient(twitchhttp.ClientConf{
		Host:           config.Resolve("partnerships-host"),
		Stats:          config.Statsd(),
		TimingXactName: "service.partnerships",
	})
}

func configureSpadeClient() (spade.Client, error) {
	httpClient := &http.Client{}
	return spade.NewClient(
		spade.InitHTTPClient(httpClient),
		spade.InitMaxConcurrency(1000),
		spade.InitStatHook(func(name string, httpStatus int, dur time.Duration) {
			err := config.Statsd().Timing(fmt.Sprintf("spade.%s.%d", name, httpStatus), int64(dur), 1.0)
			if err != nil {
				log.Printf("Error emitting spade stat %v\n", err)
			}
		}),
		spade.InitBaseURL(url.URL{
			Scheme: "https",
			Host:   config.Resolve("spade-host"),
		}),
	)
}

func configureBanReportClient() (banreport.Client, error) {
	return banreport.NewClient(config.Resolve("rails-host"), config.Statsd())
}

func configureBlacklistClient() (blacklist.Client, error) {
	return blacklist.NewClient(config.Resolve("rails-host"), config.Statsd())
}

func configureCacheClient() (cache.Client, error) {
	return cache.NewClient(config.Resolve("rails-host"), config.Statsd())
}

func configureKinesisPublisher() (kinesis.Publisher, error) {
	return kinesis.NewPublisher(
		config.Resolve("user-mutations-stream-region"),
		configResolveInt("user-mutations-stream-retry-count"),
		configResolveInt("user-mutations-stream-retry-delay-ms"),
		config.Resolve("user-mutations-stream-role"),
		config.Resolve("user-mutations-stream-name"),
		config.Statsd())
}

func configureUserBackend() (user.Backend, error) {

	sdb, err := configureDb(false)
	if err != nil {
		return nil, err
	}

	mdb, err := configureDb(true)
	if err != nil {
		return nil, err
	}

	sdbQuerier, err := configureDbQuerier(sdb)
	if err != nil {
		return nil, err
	}

	mdbQuerier, err := configureDbQuerier(mdb)
	if err != nil {
		return nil, err
	}

	redis, err := configureRedis()
	if err != nil {
		return nil, err
	}

	redicache := configureRedicache(redis)
	if err != nil {
		return nil, err
	}

	ub, err := user.NewBackend(sdbQuerier, mdbQuerier, redis, redicache, configResolveDuration("redis-user-properties-ttl-ms"))
	if err != nil {
		return nil, err
	}

	return ub, nil
}

func configureRedis() (redis.Redis, error) {

	env := config.Resolve("environment")

	conf := redicache.Config{
		Host:           config.Resolve("redis-host"),
		Port:           configResolveInt("redis-port"),
		Password:       config.Resolve("redis-pass"),
		KeyPrefix:      fmt.Sprintf("users:%v", env),
		StatsPrefix:    "redis",
		ConnectTimeout: configResolveDuration("redis-connect-timeout-ms"),
		ReadTimeout:    configResolveDuration("redis-read-timeout-ms"),
		WriteTimeout:   configResolveDuration("redis-write-timeout-ms"),
		MaxConns:       configResolveInt("redis-conn"),
	}

	redis, 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:       config.Statsd(),
	}.New()

	if err != nil {
		return nil, errors.New(fmt.Sprintf("Could not initialize redis. err=%v\n", err))
	}

	return redis, err
}

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

func configureDb(master bool) (db.DB, error) {
	password := config.Resolve("db-password")
	if password == "" {
		passwordFile := config.Resolve("db-password-file")
		if passwordFile != "" {
			passwordArray, err := ioutil.ReadFile(passwordFile)
			if err != nil {
				return nil, err
			}

			password = strings.TrimSpace(string(passwordArray))
		}
	}

	var postfix string

	if master {
		postfix = "master"
	} else {
		postfix = "slave"
	}

	log.Printf("Connecting to %v:%d as %v using DB %v",
		config.Resolve("db-host-"+postfix),
		configResolveInt("db-port-"+postfix),
		config.Resolve("db-user"),
		config.Resolve("db-name"))

	db, err := db.Open(
		db.DriverName("postgres"),
		db.Host(config.Resolve("db-host-"+postfix)),
		db.Port(configResolveInt("db-port-"+postfix)),
		db.User(config.Resolve("db-user")),
		db.Password(password),
		db.DBName(config.Resolve("db-name")),
		db.MaxOpenConns(configResolveInt("db-max-open-conns-"+postfix)),
		db.MaxIdleConns(configResolveInt("db-max-idle-conns-"+postfix)),
		db.MaxQueueSize(configResolveInt("db-max-queue-size-"+postfix)),
		db.ConnAcquireTimeout(configResolveDuration("db-conn-acquire-timeout-ms-"+postfix)),
		db.RequestTimeout(configResolveDuration("db-request-timeout-ms-"+postfix)),
		db.MaxConnAge(configResolveDuration("db-max-conn-age-ms-"+postfix)),
	)
	if err != nil {
		return nil, err
	}

	logger, err := database.NewLogger(config.Statsd(), postfix+"db.")
	db.SetCallbacks(logger.LogDBStat, logger.LogRunStat)

	go func() {
		ticker := time.Tick(10 * time.Second)
		for {
			select {
			case <-ticker:
				logger.LogDBState(db.Info())
			}
		}
	}()

	return db, nil
}

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
}

func configureDbQuerier(db db.DB) (database.Querier, error) {
	return database.NewQuerier(db)
}
