package service

import (
	"context"
	"crypto/tls"
	"crypto/x509"
	"fmt"
	"net"
	"net/url"
	"strconv"
	"time"

	"github.com/1Panel-dev/1Panel/agent/app/dto"
	"github.com/1Panel-dev/1Panel/agent/app/model"
	"github.com/1Panel-dev/1Panel/agent/app/repo"
	"go.mongodb.org/mongo-driver/v2/mongo"
	"go.mongodb.org/mongo-driver/v2/mongo/options"
	"go.mongodb.org/mongo-driver/v2/mongo/readpref"
)

type mongodbConnectionInfo struct {
	Address    string
	Port       uint
	InitialDB  string
	Username   string
	Password   string
	Timeout    uint
	SSL        bool
	RootCert   string
	ClientKey  string
	ClientCert string
	SkipVerify bool
}

func mongodbConnectionInfoFromCreate(req dto.DatabaseCreate) mongodbConnectionInfo {
	return mongodbConnectionInfo{
		Address:    req.Address,
		Port:       req.Port,
		InitialDB:  req.InitialDB,
		Username:   req.Username,
		Password:   req.Password,
		Timeout:    req.Timeout,
		SSL:        req.SSL,
		RootCert:   req.RootCert,
		ClientKey:  req.ClientKey,
		ClientCert: req.ClientCert,
		SkipVerify: req.SkipVerify,
	}
}

func mongodbConnectionInfoFromModel(db model.Database) mongodbConnectionInfo {
	return mongodbConnectionInfo{
		Address:    db.Address,
		Port:       db.Port,
		InitialDB:  db.InitialDB,
		Username:   db.Username,
		Password:   db.Password,
		Timeout:    db.Timeout,
		SSL:        db.SSL,
		RootCert:   db.RootCert,
		ClientKey:  db.ClientKey,
		ClientCert: db.ClientCert,
		SkipVerify: db.SkipVerify,
	}
}

func loadRemoteMongodbConnection(database string) (mongodbConnectionInfo, error) {
	db, err := databaseRepo.Get(repo.WithByName(database))
	if err != nil {
		return mongodbConnectionInfo{}, err
	}
	return mongodbConnectionInfoFromModel(db), nil
}

func newRemoteMongodbClient(info mongodbConnectionInfo) (*mongo.Client, context.Context, context.CancelFunc, error) {
	timeout := time.Duration(info.Timeout) * time.Second
	if timeout == 0 {
		timeout = 30 * time.Second
	}

	clientOptions := options.Client().ApplyURI(buildRemoteMongodbURI(info)).
		SetServerSelectionTimeout(timeout).
		SetConnectTimeout(timeout)
	if info.SSL {
		tlsConfig, err := buildRemoteMongodbTLSConfig(info)
		if err != nil {
			return nil, nil, nil, err
		}
		clientOptions.SetTLSConfig(tlsConfig)
	}

	ctx, cancel := context.WithTimeout(context.Background(), timeout)
	client, err := mongo.Connect(clientOptions)
	if err != nil {
		cancel()
		return nil, nil, nil, err
	}
	if err := client.Ping(ctx, readpref.Primary()); err != nil {
		_ = client.Disconnect(context.Background())
		cancel()
		return nil, nil, nil, err
	}
	return client, ctx, cancel, nil
}

func buildRemoteMongodbURI(info mongodbConnectionInfo) string {
	uri := url.URL{
		Scheme: "mongodb",
		Host:   net.JoinHostPort(info.Address, strconv.Itoa(int(info.Port))),
		Path:   "/",
	}
	uri.User = url.UserPassword(info.Username, info.Password)
	query := url.Values{}
	authSource := info.InitialDB
	if authSource == "" {
		authSource = "admin"
	}
	query.Set("authSource", authSource)
	query.Set("directConnection", "true")
	uri.RawQuery = query.Encode()
	return uri.String()
}

func buildRemoteMongodbTLSConfig(info mongodbConnectionInfo) (*tls.Config, error) {
	tlsConfig := &tls.Config{
		MinVersion:         tls.VersionTLS12,
		InsecureSkipVerify: info.SkipVerify,
	}

	if info.RootCert != "" {
		pool := x509.NewCertPool()
		if !pool.AppendCertsFromPEM([]byte(info.RootCert)) {
			return nil, fmt.Errorf("load mongodb ca cert failed")
		}
		tlsConfig.RootCAs = pool
	}

	if info.ClientCert != "" && info.ClientKey != "" {
		cert, err := tls.X509KeyPair([]byte(info.ClientCert), []byte(info.ClientKey))
		if err != nil {
			return nil, err
		}
		tlsConfig.Certificates = []tls.Certificate{cert}
	}

	return tlsConfig, nil
}
