package main

import (
	"context"
	"errors"
	"io"
	"log"
	"net"
	"net/url"
	"os"
	"os/signal"
	"strings"
	"syscall"
	"time"
	"unicode"

	"github.com/pingcap-inc/nexuscrm-api-host/processhttp"
	"github.com/pingcap-inc/nexuscrm-api-host/runtimegrpc"
	"github.com/pingcap-inc/nexuscrm-app-packs/workers/mcp/internal/worker"
	identity "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/identityv1"
	query "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/queryv1"
	mutation "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/recordmutationv1"
)

var version = "dev"

func main() {
	if err := run(); err != nil {
		log.Fatal(err)
	}
}

func run() error {
	ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
	defer stop()
	config, origin, address, err := loadConfig(os.Getenv)
	if err != nil {
		return err
	}
	startup, cancel := context.WithTimeout(ctx, 30*time.Second)
	defer cancel()
	clients := runtimegrpc.NewRuntimeClientGroup(config)
	platform, err := clients.DialPlatformRuntime(startup)
	if err != nil {
		return errors.New("MCP Worker: Runtime identity connection failed")
	}
	defer platform.Close()
	queries, err := clients.DialQueryRuntime(startup)
	if err != nil {
		return errors.New("MCP Worker: Runtime query connection failed")
	}
	defer queries.Close()
	writes, err := clients.DialRecordMutationRuntime(startup)
	if err != nil {
		return errors.New("MCP Worker: Runtime mutation connection failed")
	}
	defer writes.Close()
	cancel()
	service, err := worker.New(worker.Config{PublicOrigin: origin, Version: version, RequestTimeout: 30 * time.Second,
		Ports: worker.Ports{Authorize: platform.AuthorizeCapabilities, Query: queries.Query, Mutate: writes.Mutate, Status: writes.Status}})
	if err != nil {
		return err
	}
	process, err := processhttp.NewProcess(processhttp.ProcessConfig{
		Role: "mcp-worker", Version: version, Address: address, Handler: service,
		Capabilities: []string{identity.CapabilityName, query.CapabilityName, mutation.CapabilityName},
		Ready:        func() bool { return platform.Ready() && queries.Ready() && writes.Ready() },
		BeginDrain:   service.BeginDrain,
		Policy:       processhttp.ServerPolicy{ReadHeaderTimeout: 5 * time.Second, ReadTimeout: 35 * time.Second, WriteTimeout: 35 * time.Second, IdleTimeout: 60 * time.Second},
	})
	if err != nil {
		return err
	}
	if err := process.Start(); err != nil {
		return errors.New("MCP Worker: HTTP listener failed")
	}
	done := make(chan error, 1)
	go func() { done <- process.Wait() }()
	select {
	case err := <-done:
		return err
	case <-ctx.Done():
		shutdown, cancel := context.WithTimeout(context.Background(), 35*time.Second)
		defer cancel()
		return process.Shutdown(shutdown)
	}
}

func loadConfig(getenv func(string) string) (runtimegrpc.RuntimeClientConfig, string, string, error) {
	config := runtimegrpc.RuntimeClientConfig{
		Target: getenv("MCP_RUNTIME_TARGET"), AppInstallationID: getenv("MCP_APP_INSTALLATION_ID"),
		CAFile: getenv("MCP_RUNTIME_CA_FILE"), ServerName: getenv("MCP_RUNTIME_SERVER_NAME"),
		ClientCertificateFile: getenv("MCP_RUNTIME_CLIENT_CERT_FILE"), ClientKeyFile: getenv("MCP_RUNTIME_CLIENT_KEY_FILE"),
		AttemptTimeout: 10 * time.Second,
	}
	origin, address := getenv("MCP_PUBLIC_ORIGIN"), getenv("MCP_HTTP_ADDRESS")
	if address == "" {
		address = "127.0.0.1:8092"
	}
	if config.Target == "" || config.AppInstallationID == "" || origin == "" || getenv("MCP_WORKLOAD_TOKEN_FILE") == "" {
		return config, origin, address, errors.New("MCP Worker: Runtime target, installation, public origin and workload token file are required")
	}
	publicURL, err := url.Parse(origin)
	if err != nil {
		return config, origin, address, errors.New("MCP Worker: invalid public origin")
	}
	if publicURL.Scheme == "http" {
		host, _, err := net.SplitHostPort(address)
		ip := net.ParseIP(host)
		// Require a literal loopback bind, not a hostname that may resolve to a
		// different interface. HTTPS origins use the documented trusted TLS ingress.
		if err != nil || ip == nil || !ip.IsLoopback() {
			return config, origin, address, errors.New("MCP Worker: HTTP origin requires a literal loopback listener")
		}
	}
	config.WorkloadToken, err = readWorkloadToken(getenv("MCP_WORKLOAD_TOKEN_FILE"))
	if err != nil {
		return config, origin, address, err
	}

	return config, origin, address, nil
}

// Only the opened regular, private file is read. NOFOLLOW and NONBLOCK avoid
// symlink replacement and FIFO/device hangs between path inspection and open.
func readWorkloadToken(path string) (string, error) {
	const maxBytes = 64 * 1024
	invalid := errors.New("MCP Worker: workload token requires a private regular file containing one non-empty token")
	file, err := os.OpenFile(path, os.O_RDONLY|syscall.O_NOFOLLOW|syscall.O_NONBLOCK, 0)
	if err != nil {
		return "", invalid
	}
	defer file.Close()
	info, err := file.Stat()
	if err != nil || !info.Mode().IsRegular() || info.Mode().Perm()&0o077 != 0 || info.Size() > maxBytes {
		return "", invalid
	}
	raw, err := io.ReadAll(io.LimitReader(file, maxBytes+1))
	if err != nil || len(raw) > maxBytes {
		return "", invalid
	}
	token := strings.TrimSuffix(strings.TrimSuffix(string(raw), "\n"), "\r")
	if token == "" || strings.IndexFunc(token, unicode.IsSpace) >= 0 {
		return "", invalid
	}
	return token, nil
}
