package main

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"os"
	"os/signal"
	"strconv"
	"strings"
	"syscall"
	"time"

	"lingniu-vehicle-ingest/go/vehicle-gateway/internal/feichibridge"
	"lingniu-vehicle-ingest/go/vehicle-gateway/internal/health"
	"lingniu-vehicle-ingest/go/vehicle-gateway/internal/metrics"
	"lingniu-vehicle-ingest/go/vehicle-gateway/internal/observability"
)

type authSecret struct {
	Username string `json:"username"`
	Password string `json:"password"`
}

type targetSecret struct {
	PlatformID string `json:"platformId"`
	Username   string `json:"username"`
	Password   string `json:"password"`
}

type config struct {
	BaseURL         string
	AuthSecretFile  string
	TargetSecret    string
	TargetAddress   string
	StateFile       string
	HealthAddress   string
	HTTPTimeout     time.Duration
	TargetTimeout   time.Duration
	OCRImage        string
	OCRTimeout      time.Duration
	LoginAttempts   int
	LoginRetryDelay time.Duration
	Service         feichibridge.ServiceConfig
}

func main() {
	logger := observability.NewLogger("feichi-bridge")
	cfg, err := loadConfig()
	if err != nil {
		logger.Error("load configuration failed", "error", err)
		os.Exit(1)
	}
	auth, err := readJSONSecret[authSecret](cfg.AuthSecretFile)
	if err != nil {
		logger.Error("read Feichi API secret failed", "error", err)
		os.Exit(1)
	}
	credentials, err := readJSONSecret[targetSecret](cfg.TargetSecret)
	if err != nil {
		logger.Error("read GB/T 32960 target secret failed", "error", err)
		os.Exit(1)
	}
	source, err := feichibridge.NewAuthenticatedAPIClient(
		cfg.BaseURL,
		feichibridge.LoginCredentials{
			Username: auth.Username, Password: auth.Password,
			MaxAttempts: cfg.LoginAttempts, RetryDelay: cfg.LoginRetryDelay,
		},
		feichibridge.DockerCaptchaSolver{Image: cfg.OCRImage, Timeout: cfg.OCRTimeout},
		cfg.HTTPTimeout,
	)
	if err != nil {
		logger.Error("build Feichi API client failed", "error", err)
		os.Exit(1)
	}
	state, err := feichibridge.OpenStateStore(cfg.StateFile)
	if err != nil {
		logger.Error("open bridge state failed", "error", err)
		os.Exit(1)
	}
	target, err := feichibridge.NewTarget(feichibridge.TargetConfig{
		Address: cfg.TargetAddress, PlatformID: credentials.PlatformID,
		Username: credentials.Username, Password: credentials.Password,
		Timeout: cfg.TargetTimeout,
	}, state)
	if err != nil {
		logger.Error("build GB/T 32960 target failed", "error", err)
		os.Exit(1)
	}
	registry := metrics.NewRegistry()
	service, err := feichibridge.NewService(cfg.Service, source, target, state, logger, registry)
	if err != nil {
		logger.Error("build bridge service failed", "error", err)
		os.Exit(1)
	}

	ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
	defer stop()
	health.Start(ctx, logger, health.NewServer(cfg.HealthAddress, "feichi-bridge", []health.Check{
		{Name: "bridge", Check: service.Ready},
	}, registry))
	logger.Info("Feichi bridge starting",
		"base_url", cfg.BaseURL,
		"target_address", cfg.TargetAddress,
		"poll_interval", cfg.Service.PollInterval,
		"backfill_enabled", cfg.Service.BackfillEnabled,
	)
	if strings.HasPrefix(strings.ToLower(cfg.BaseURL), "http://") {
		logger.Warn("Feichi source uses clear-text HTTP; deploy only through a controlled egress path")
	}
	if err := service.Run(ctx); err != nil && !errors.Is(err, context.Canceled) {
		logger.Error("Feichi bridge stopped unexpectedly", "error", err)
		os.Exit(1)
	}
	logger.Info("Feichi bridge stopped")
}

func loadConfig() (config, error) {
	cfg := config{
		BaseURL:         strings.TrimSpace(os.Getenv("FEICHI_BASE_URL")),
		AuthSecretFile:  env("FEICHI_AUTH_SECRET_FILE", strings.TrimSpace(os.Getenv("FEICHI_AUTH_HEADERS_FILE"))),
		TargetSecret:    strings.TrimSpace(os.Getenv("FEICHI_TARGET_SECRET_FILE")),
		TargetAddress:   env("FEICHI_TARGET_ADDR", "127.0.0.1:32960"),
		StateFile:       env("FEICHI_STATE_FILE", "/var/lib/lingniu-feichi-bridge/state.json"),
		HealthAddress:   env("HEALTH_ADDR", "127.0.0.1:20219"),
		HTTPTimeout:     seconds("FEICHI_HTTP_TIMEOUT_SECONDS", 15),
		TargetTimeout:   seconds("FEICHI_TARGET_TIMEOUT_SECONDS", 10),
		OCRImage:        env("FEICHI_OCR_IMAGE", "lingniu/feichi-captcha-ocr:1.0.0"),
		OCRTimeout:      seconds("FEICHI_OCR_TIMEOUT_SECONDS", 20),
		LoginAttempts:   envInt("FEICHI_LOGIN_MAX_ATTEMPTS", 20),
		LoginRetryDelay: seconds("FEICHI_LOGIN_RETRY_SECONDS", 1),
		Service: feichibridge.ServiceConfig{
			PollInterval:        seconds("FEICHI_POLL_INTERVAL_SECONDS", 10),
			DiscoveryInterval:   seconds("FEICHI_DISCOVERY_INTERVAL_SECONDS", 300),
			BackfillInterval:    seconds("FEICHI_BACKFILL_INTERVAL_SECONDS", 3600),
			BackfillLookback:    seconds("FEICHI_BACKFILL_LOOKBACK_SECONDS", 3600),
			BackfillWindow:      seconds("FEICHI_BACKFILL_WINDOW_SECONDS", 1200),
			BackfillSafetyLag:   seconds("FEICHI_BACKFILL_SAFETY_SECONDS", 30),
			SourceStaleAfter:    seconds("FEICHI_SOURCE_STALE_SECONDS", 120),
			FetchConcurrency:    envInt("FEICHI_FETCH_CONCURRENCY", 4),
			BackfillEnabled:     envBool("FEICHI_BACKFILL_ENABLED", true),
			StaleReissueEnabled: envBool("FEICHI_STALE_REISSUE_ENABLED", true),
		},
	}
	var missing []string
	if cfg.BaseURL == "" {
		missing = append(missing, "FEICHI_BASE_URL")
	}
	if cfg.AuthSecretFile == "" {
		missing = append(missing, "FEICHI_AUTH_SECRET_FILE")
	}
	if cfg.TargetSecret == "" {
		missing = append(missing, "FEICHI_TARGET_SECRET_FILE")
	}
	if len(missing) > 0 {
		return config{}, fmt.Errorf("required configuration missing: %s", strings.Join(missing, ", "))
	}
	if cfg.Service.BackfillWindow > 24*time.Hour {
		return config{}, errors.New("FEICHI_BACKFILL_WINDOW_SECONDS must not exceed 86400")
	}
	return cfg, nil
}

func readJSONSecret[T any](path string) (T, error) {
	var value T
	encoded, err := os.ReadFile(path)
	if err != nil {
		return value, err
	}
	if err := json.Unmarshal(encoded, &value); err != nil {
		return value, fmt.Errorf("decode %s: %w", path, err)
	}
	return value, nil
}

func env(name, fallback string) string {
	if value := strings.TrimSpace(os.Getenv(name)); value != "" {
		return value
	}
	return fallback
}

func envInt(name string, fallback int) int {
	value := strings.TrimSpace(os.Getenv(name))
	if value == "" {
		return fallback
	}
	parsed, err := strconv.Atoi(value)
	if err != nil || parsed <= 0 {
		return fallback
	}
	return parsed
}

func envBool(name string, fallback bool) bool {
	value := strings.TrimSpace(os.Getenv(name))
	if value == "" {
		return fallback
	}
	parsed, err := strconv.ParseBool(value)
	if err != nil {
		return fallback
	}
	return parsed
}

func seconds(name string, fallback int) time.Duration {
	return time.Duration(envInt(name, fallback)) * time.Second
}
