package grpcimp

import (
	"context"
	"crypto/tls"
	"crypto/x509"
	"errors"
	"net"
	"os"
	"sync"

	"github.com/tech/sendico/pkg/mlogger"
	"go.uber.org/zap"
	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials"
	"google.golang.org/grpc/health"
	healthpb "google.golang.org/grpc/health/grpc_health_v1"
	"google.golang.org/grpc/reflection"
)

type routerError string

func (e routerError) Error() string {
	return string(e)
}

type routerCauseError struct {
	message string
	cause   error
}

func (e *routerCauseError) Error() string {
	if e == nil {
		return ""
	}
	if e.cause == nil {
		return e.message
	}
	return e.message + ": " + e.cause.Error()
}

func (e *routerCauseError) Unwrap() error {
	if e == nil {
		return nil
	}
	return e.cause
}

func newRouterErrorWithCause(message string, cause error) error {
	return &routerCauseError{
		message: message,
		cause:   cause,
	}
}

const (
	errMsgAlreadyStarted           = "grpc router already started"
	errMsgListenFailed             = "failed to listen on requested address"
	errMsgNilContext               = "nil context"
	errMsgTLSMissingCertAndKey     = "tls configuration requires cert_file and key_file"
	errMsgLoadServerCertificate    = "failed to load server certificate"
	errMsgReadCAFile               = "failed to read CA file"
	errMsgAppendCACertificates     = "failed to append CA certificates"
	errMsgClientCertRequiresCAFile = "client certificate verification requested but ca_file is empty"
)

var (
	errAlreadyStarted           = routerError(errMsgAlreadyStarted)
	errNilContext               = routerError(errMsgNilContext)
	errTLSMissingCertAndKey     = routerError(errMsgTLSMissingCertAndKey)
	errAppendCACertificates     = routerError(errMsgAppendCACertificates)
	errClientCertRequiresCAFile = routerError(errMsgClientCertRequiresCAFile)
)

type Router struct {
	logger    mlogger.Logger
	config    Config
	server    *grpc.Server
	listener  net.Listener
	options   *Options
	mu        sync.RWMutex
	started   bool
	serveErr  chan error
	healthSrv *health.Server
}

func NewRouter(logger mlogger.Logger, cfg *Config, opts *Options) (*Router, error) {
	if cfg == nil {
		cfg = &Config{}
	}
	if opts == nil {
		opts = &Options{}
	}

	network := cfg.Network
	if network == "" {
		network = "tcp"
	}
	address := cfg.Address
	if address == "" {
		address = ":0"
	}

	listener := opts.Listener
	var err error
	if listener == nil {
		listener, err = (&net.ListenConfig{}).Listen(context.Background(), network, address)
		if err != nil {
			return nil, newRouterErrorWithCause(errMsgListenFailed, err)
		}
	}

	serverOpts := make([]grpc.ServerOption, 0, len(opts.ServerOptions)+4)
	serverOpts = append(serverOpts, opts.ServerOptions...)

	if cfg.MaxRecvMsgSize > 0 {
		serverOpts = append(serverOpts, grpc.MaxRecvMsgSize(cfg.MaxRecvMsgSize))
	}
	if cfg.MaxSendMsgSize > 0 {
		serverOpts = append(serverOpts, grpc.MaxSendMsgSize(cfg.MaxSendMsgSize))
	}

	if cfg.TLS != nil {
		creds, err := configureTLS(cfg.TLS)
		if err != nil {
			return nil, err
		}
		serverOpts = append(serverOpts, grpc.Creds(creds))
	}

	unaryInterceptors := append([]grpc.UnaryServerInterceptor{prometheusUnaryInterceptor()}, opts.UnaryInterceptors...)
	streamInterceptors := append([]grpc.StreamServerInterceptor{prometheusStreamInterceptor()}, opts.StreamInterceptors...)

	if len(unaryInterceptors) > 0 {
		serverOpts = append(serverOpts, grpc.ChainUnaryInterceptor(unaryInterceptors...))
	}
	if len(streamInterceptors) > 0 {
		serverOpts = append(serverOpts, grpc.ChainStreamInterceptor(streamInterceptors...))
	}

	srv := grpc.NewServer(serverOpts...)
	r := &Router{
		logger:   logger.Named("grpc"),
		config:   *cfg,
		server:   srv,
		listener: listener,
		options:  opts,
		serveErr: make(chan error, 1),
	}

	if cfg.EnableReflection {
		reflection.Register(srv)
	}
	if cfg.EnableHealth {
		r.healthSrv = health.NewServer()
		r.healthSrv.SetServingStatus("", healthpb.HealthCheckResponse_NOT_SERVING)
		healthpb.RegisterHealthServer(srv, r.healthSrv)
	}

	return r, nil
}

func (r *Router) Register(registration func(grpc.ServiceRegistrar)) error {
	r.mu.Lock()
	defer r.mu.Unlock()
	if r.started {
		return errAlreadyStarted
	}

	registration(r.server)
	return nil
}

func (r *Router) Start(ctx context.Context) error {
	if ctx == nil {
		return errNilContext
	}

	r.mu.Lock()
	if r.started {
		r.mu.Unlock()
		return errAlreadyStarted
	}
	r.started = true
	r.mu.Unlock()

	if r.healthSrv != nil {
		r.healthSrv.SetServingStatus("", healthpb.HealthCheckResponse_SERVING)
	}

	go func() {
		<-ctx.Done()
		r.logger.Info("Context cancelled, stopping gRPC server")
		r.server.GracefulStop()
	}()

	go func() {
		err := r.server.Serve(r.listener)
		if err != nil && !errors.Is(err, grpc.ErrServerStopped) {
			select {
			case r.serveErr <- err:
			default:
				r.logger.Error("Failed to report gRPC serve error", zap.Error(err))
			}
		}
		close(r.serveErr)
	}()

	r.logger.Info("GRPC server started", zap.String("network", r.listener.Addr().Network()), zap.String("address", r.listener.Addr().String()))
	return nil
}

func (r *Router) Finish(ctx context.Context) error {
	if ctx == nil {
		return errNilContext
	}

	r.mu.RLock()
	started := r.started
	r.mu.RUnlock()
	if !started {
		return nil
	}

	if r.healthSrv != nil {
		r.healthSrv.SetServingStatus("", healthpb.HealthCheckResponse_NOT_SERVING)
	}

	done := make(chan struct{})
	go func() {
		r.server.GracefulStop()
		close(done)
	}()

	select {
	case <-done:
	case <-ctx.Done():
		r.logger.Warn("Graceful stop timed out, forcing stop", zap.Error(ctx.Err()))
		r.server.Stop()
		return ctx.Err()
	}

	if err, ok := <-r.serveErr; ok {
		return err
	}
	return nil
}

func (r *Router) Addr() net.Addr {
	return r.listener.Addr()
}

func (r *Router) Done() <-chan error {
	return r.serveErr
}

func configureTLS(cfg *TLSConfig) (credentials.TransportCredentials, error) {
	if cfg.CertFile == "" || cfg.KeyFile == "" {
		return nil, errTLSMissingCertAndKey
	}

	certificate, err := tls.LoadX509KeyPair(cfg.CertFile, cfg.KeyFile)
	if err != nil {
		return nil, newRouterErrorWithCause(errMsgLoadServerCertificate, err)
	}

	tlsCfg := &tls.Config{
		Certificates: []tls.Certificate{certificate},
		MinVersion:   tls.VersionTLS12,
	}

	if cfg.CAFile != "" {
		caPem, err := os.ReadFile(cfg.CAFile)
		if err != nil {
			return nil, newRouterErrorWithCause(errMsgReadCAFile, err)
		}

		certPool := x509.NewCertPool()
		if ok := certPool.AppendCertsFromPEM(caPem); !ok {
			return nil, errAppendCACertificates
		}
		tlsCfg.ClientCAs = certPool
		if cfg.RequireClientCert {
			tlsCfg.ClientAuth = tls.RequireAndVerifyClientCert
		}
	} else if cfg.RequireClientCert {
		return nil, errClientCertRequiresCAFile
	}

	return credentials.NewTLS(tlsCfg), nil
}
