/*
 * Copyright 2021 PingCAP, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package main

import (
	"context"
	"crypto/tls"
	"flag"
	"io"
	olog "log"
	"net/http"
	"os"
	"os/signal"
	"strings"
	"syscall"
	"time"

	"github.com/pingcap/mimir/pkg/event"
	"github.com/pingcap/mimir/pkg/event/event_bus"

	"github.com/gorilla/mux"
	"github.com/pingcap/fn"
	"github.com/pingcap/log"
	"go.etcd.io/etcd/client/pkg/v3/transport"
	_ "go.uber.org/automaxprocs"
	"go.uber.org/zap"

	"github.com/pingcap/mimir/config"
	"github.com/pingcap/mimir/logger"
	"github.com/pingcap/mimir/model"
	"github.com/pingcap/mimir/pkg/endpoint"
	"github.com/pingcap/mimir/pkg/middleware/prom"
	"github.com/pingcap/mimir/pkg/service"
	"github.com/pingcap/mimir/pkg/service/auth"
	"github.com/pingcap/mimir/util"
)

var (
	cfgPath string
)

func main() {
	flag.StringVar(&cfgPath, "c", "./", "mimird -c=/path/to/config.yaml")

	flag.Parse()
	if err := config.InitOption(cfgPath); err != nil {
		log.Fatal("parse config failed", zap.Error(err))
	}

	if err := logger.Init(config.Options.Log); err != nil {
		log.Fatal("init log error", zap.Error(err))
	}

	router := mux.NewRouter()
	fn.SetErrorEncoder(util.EncodeError)
	fn.SetResponseEncoder(util.EncodeResp)

	// mount middleware on router
	router.HandleFunc("/ping", endpoint.Ping)
	router.Use(prom.MetricsMiddleware)

	prom.RegisterOnMux(router)

	// register subscriber
	cli, err := config.Options.K8S.BuildControllerClient()
	if err != nil {
		log.Fatal("init rest config error", zap.Error(err))
	}
	event_bus.EventBus.Register(&event.TableCheckerSubscriber{
		Cli:               cli,
		Image:             config.Options.TableChecker.Image,
		DbSecretName:      config.Options.Sink.DBSecret,
		DbSecretNamespace: config.Options.Sink.Namespace,
	})

	// init service and endpoint
	clientSet, err := config.Options.K8S.BuildClientset()
	if err != nil {
		log.Fatal("build clientset error", zap.Error(err))
	}
	db, err := model.Dial(config.Options.MySql)
	if err != nil {
		log.Fatal("connect to database error", zap.Error(err))
	}
	if err := model.Migrate(db); err != nil {
		log.Fatal("migrate error", zap.Error(err))
	}
	jobDao, err := model.NewJobDao(model.WithDB(db))
	if err != nil {
		log.Fatal("build job dao error", zap.Error(err))
	}
	sinkDao, err := model.NewSinkDao(model.WithDB(db))
	if err != nil {
		log.Fatal("build sink dao error", zap.Error(err))
	}
	savepointDao, err := model.NewSavepointDao(model.WithDB(db))
	if err != nil {
		log.Fatal("build savepoint dao error", zap.Error(err))
	}
	ingressDao, err := model.NewIngressDao(model.WithDB(db))
	if err != nil {
		log.Fatal("build savepoint dao error", zap.Error(err))
	}
	// TODO: refactor to be structural
	svc, err := service.New(
		service.K8sConfigFileOpt(config.Options.K8S.KubeConfigPath),
		service.K8sClientOpt(clientSet),
		service.FlinkClientPathOpt(config.Options.Flink.ClientPath),
		service.FlinkImageOpt(config.Options.Flink.Image),
		service.FlinkSessionOpt(config.Options.Flink.SessionEndpoint),
		service.ExecutorCMDOpt(config.Options.Flink.CMD),
		service.FlinkRestExposeType(config.Options.Flink.RestExposedType),
		service.FlinkClusterServiceAccountOpt(config.Options.Flink.ServiceAccount),
		service.FlinkPersistenceOpt(config.Options.Flink.Persistence),
		service.JobDaoOpt(jobDao),
		service.SinkDaoOpt(sinkDao),
		service.SavepointDaoOpt(savepointDao),
		service.KafkaOpt(config.Options.Kafka),
		service.IngressDaoOpt(ingressDao),
		service.BizDsnOpt(cli, config.Options.Sink.DBSecret, config.Options.Sink.Namespace),
		service.MapperOpt(config.Options.Mappers),
	)
	if err != nil {
		log.Fatal("init service error", zap.Error(err))
	}
	ep, err := endpoint.New(svc)
	if err != nil {
		log.Fatal("init endpoint error", zap.Error(err))
	}

	var tlsCfg *tls.Config
	// check if security is configured
	if config.Options.Security != (config.SecurityConfig{}) {
		tlsCfg, err = transport.TLSInfo{
			TrustedCAFile: config.Options.Security.TrustedCAFile,
			CertFile:      config.Options.Security.CertFile,
			KeyFile:       config.Options.Security.KeyFile,
		}.ServerConfig()
		if err != nil {
			log.Fatal("load tls config", zap.Error(err))
		}
	}
	fn.Plugin(auth.Auth(tlsCfg))

	// mount api handler on router
	ep.MountOnMux(router)
	httpServer := &http.Server{
		Addr:              config.Options.Server.Addr,
		Handler:           router,
		ReadHeaderTimeout: time.Duration(config.Options.Server.RTimeout) * time.Second,
		ReadTimeout:       time.Duration(config.Options.Server.RTimeout) * time.Second,
		WriteTimeout:      time.Duration(config.Options.Server.WTimeout) * time.Second,
		IdleTimeout:       time.Duration(config.Options.Server.DTimeout) * time.Second,
		MaxHeaderBytes:    config.Options.Server.MaxHeaderBytes,
		TLSConfig:         tlsCfg,
		ErrorLog:          olog.New(&tlsHandshakeErrorWriter{os.Stderr}, "", 0),
	}
	go func() {
		// serve connections
		if tlsCfg != nil {
			if err := httpServer.ListenAndServeTLS("", ""); err != nil && err != http.ErrServerClosed {
				log.Fatal("listen error", zap.Error(err))
			}
		} else {
			if err := httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {
				log.Fatal("listen error", zap.Error(err))
			}
		}
	}()

	log.Info("starting listen", zap.String("endpoint", config.Options.Server.Addr), zap.Bool("tls enabled", tlsCfg != nil))
	// Wait for interrupt signal to gracefully shutdown the server with
	// a timeout of 5 seconds.
	ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
	<-ctx.Done()
	stop()
	log.Info("server shutdown ...")

	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()
	if err := httpServer.Shutdown(ctx); err != nil {
		log.Fatal("shutdown error", zap.Error(err))
	}
	log.Info("server exiting")
}

type tlsHandshakeErrorWriter struct {
	out io.Writer
}

const tlsHandshakeErrorPrefix = "http: TLS handshake error"

func (w *tlsHandshakeErrorWriter) Write(p []byte) (int, error) {
	if strings.Contains(string(p), tlsHandshakeErrorPrefix) {
		return len(p), nil
	}

	// for non tls handshake error, log it as usual
	return w.out.Write(p)
}
