package config

import (
	"context"
	"fmt"
	"os"
	"path/filepath"
	"runtime"
	"strconv"
	"strings"
)

const (
	defaultRedisURL             = "redis://localhost:6379"
	defaultArtifactStorePath    = "./data/artifacts"
	defaultConsumerGroup        = "commit-intelligence-service"
	defaultServiceVersion       = "v0.1.0"
	defaultMongoURI             = "mongodb://localhost:27018/adpilot_commit_intel"
	defaultMongoDatabase        = "adpilot_commit_intel"
	defaultDeltasMongoDatabase  = "adpilot_code_parser"
	defaultIndexingRunsDatabase = "adpilot_repo_sync"
	localEnvironment            = "local"
)

// Config holds runtime configuration for the service.
type Config struct {
	ServiceName string
	Host        string
	Port        int
	LogLevel    string
	LogFile     string
	Environment string
	Redis       RedisConfig
	Mongo       MongoConfig
	Artifacts   ArtifactsConfig
	Analysis    AnalysisConfig
	Stream      StreamConsumerConfig
}

// MongoConfig holds MongoDB and cross-database settings.
type MongoConfig struct {
	URI                  string
	Database             string
	DeltasDatabase       string
	IndexingRunsDatabase string
	IndexingRunsEnabled  bool
}

// RedisConfig holds Redis Streams connection settings.
type RedisConfig struct {
	URL           string
	StreamPrefix  string
	ConsumerGroup string
}

// ArtifactsConfig holds local graph artifact and delta storage settings.
type ArtifactsConfig struct {
	StorePath string
}

// AnalysisConfig holds commit analysis runtime settings.
type AnalysisConfig struct {
	Workers        int
	ServiceVersion string
}

// StreamConsumerConfig holds Redis stream consumer retry settings.
type StreamConsumerConfig struct {
	Group              string
	ConsumerName       string
	MaxRetries         int
	AutoclaimMinIdleMs int
	AutoclaimCount     int64
	BlockMs            int
	ReadCount          int64
}

// StreamName returns the Redis stream name with an optional environment prefix.
func (r RedisConfig) StreamName(base string) string {
	if r.StreamPrefix == "" {
		return base
	}
	prefix := r.StreamPrefix
	if !strings.HasSuffix(prefix, ".") {
		prefix += "."
	}
	return prefix + base
}

// EnsureStorePath creates the artifact store directory when missing.
func (a ArtifactsConfig) EnsureStorePath() error {
	path := strings.TrimSpace(a.StorePath)
	if path == "" {
		return fmt.Errorf("artifact store path is required")
	}

	absPath, err := filepath.Abs(path)
	if err != nil {
		return fmt.Errorf("resolve artifact store path: %w", err)
	}
	if err := os.MkdirAll(absPath, 0o755); err != nil {
		return fmt.Errorf("create artifact store directory: %w", err)
	}
	return nil
}

// Ready reports whether the artifact store directory exists and is writable.
func (a ArtifactsConfig) Ready(ctx context.Context) bool {
	if err := ctx.Err(); err != nil {
		return false
	}

	path := strings.TrimSpace(a.StorePath)
	if path == "" {
		return false
	}

	absPath, err := filepath.Abs(path)
	if err != nil {
		return false
	}

	info, err := os.Stat(absPath)
	if err != nil || !info.IsDir() {
		return false
	}

	testFile := filepath.Join(absPath, ".ready_check")
	file, err := os.OpenFile(testFile, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0o644)
	if err != nil {
		return false
	}
	_ = file.Close()
	_ = os.Remove(testFile)
	return true
}

// Load reads configuration from environment variables with defaults.
func Load(serviceName string, defaultPort int) (Config, error) {
	port := defaultPort
	if raw := os.Getenv("PORT"); raw != "" {
		value, err := strconv.Atoi(raw)
		if err != nil {
			return Config{}, fmt.Errorf("invalid PORT: %w", err)
		}
		port = value
	}

	environment := getenv("ENVIRONMENT", localEnvironment)

	workers, err := loadAnalysisWorkers()
	if err != nil {
		return Config{}, err
	}

	cfg := Config{
		ServiceName: serviceName,
		Host:        getenv("HOST", "0.0.0.0"),
		Port:        port,
		LogLevel:    getenv("LOG_LEVEL", "info"),
		LogFile:     strings.TrimSpace(os.Getenv("LOG_FILE")),
		Environment: environment,
		Redis: RedisConfig{
			URL:           loadWithLocalDefault("REDIS_URL", defaultRedisURL, environment),
			StreamPrefix:  os.Getenv("REDIS_STREAM_PREFIX"),
			ConsumerGroup: loadWithLocalDefault("CONSUMER_GROUP", defaultConsumerGroup, environment),
		},
		Mongo: MongoConfig{
			URI:                  loadWithLocalDefault("MONGO_URI", defaultMongoURI, environment),
			Database:             loadWithLocalDefault("MONGODB_DATABASE", defaultMongoDatabase, environment),
			DeltasDatabase:       loadWithLocalDefault("DELTAS_MONGODB_DATABASE", defaultDeltasMongoDatabase, environment),
			IndexingRunsDatabase: loadWithLocalDefault("INDEXING_RUNS_DATABASE", defaultIndexingRunsDatabase, environment),
			IndexingRunsEnabled:  loadBoolDefault("INDEXING_RUNS_ENABLED", true),
		},
		Stream: StreamConsumerConfig{
			Group:              getenv("REDIS_STREAM_GROUP", ""),
			ConsumerName:       os.Getenv("REDIS_CONSUMER_NAME"),
			MaxRetries:         loadIntDefault("REDIS_STREAM_MAX_RETRIES", 5),
			AutoclaimMinIdleMs: loadIntDefault("REDIS_STREAM_AUTOCLAIM_MS", 60000),
			AutoclaimCount:     int64(loadIntDefault("REDIS_STREAM_AUTOCLAIM_COUNT", 10)),
			BlockMs:            loadIntDefault("REDIS_STREAM_BLOCK_MS", 2000),
			ReadCount:          int64(loadIntDefault("REDIS_STREAM_COUNT", 10)),
		},
		Artifacts: ArtifactsConfig{
			StorePath: loadWithLocalDefault("ARTIFACT_STORE_PATH", defaultArtifactStorePath, environment),
		},
		Analysis: AnalysisConfig{
			Workers:        workers,
			ServiceVersion: loadWithLocalDefault("SERVICE_VERSION", defaultServiceVersion, environment),
		},
	}

	if cfg.Stream.Group == "" {
		cfg.Stream.Group = cfg.Redis.ConsumerGroup
	}

	if cfg.Environment != localEnvironment {
		if err := cfg.Validate(); err != nil {
			return Config{}, err
		}
	}

	return cfg, nil
}

// Validate checks required configuration for non-local environments.
func (c Config) Validate() error {
	checks := []struct {
		env   string
		value string
	}{
		{"REDIS_URL", c.Redis.URL},
		{"MONGO_URI", c.Mongo.URI},
		{"MONGODB_DATABASE", c.Mongo.Database},
	}

	var missing []string
	for _, check := range checks {
		if strings.TrimSpace(check.value) == "" {
			missing = append(missing, check.env)
		}
	}

	if len(missing) == 0 {
		return nil
	}
	return fmt.Errorf("missing required configuration: %s", strings.Join(missing, ", "))
}

func (c Config) Address() string {
	return fmt.Sprintf("%s:%d", c.Host, c.Port)
}

func loadAnalysisWorkers() (int, error) {
	raw := os.Getenv("ANALYSIS_WORKERS")
	if raw == "" {
		return runtime.NumCPU(), nil
	}

	workers, err := strconv.Atoi(raw)
	if err != nil {
		return 0, fmt.Errorf("invalid ANALYSIS_WORKERS: %w", err)
	}
	if workers < 1 {
		return 0, fmt.Errorf("invalid ANALYSIS_WORKERS: must be >= 1")
	}
	return workers, nil
}

func getenv(key, fallback string) string {
	if value := os.Getenv(key); value != "" {
		return value
	}
	return fallback
}

func loadWithLocalDefault(key, fallback, environment string) string {
	if value := os.Getenv(key); value != "" {
		return value
	}
	if environment == localEnvironment {
		return fallback
	}
	return ""
}

func loadIntDefault(key string, fallback int) int {
	raw := os.Getenv(key)
	if raw == "" {
		return fallback
	}
	value, err := strconv.Atoi(raw)
	if err != nil {
		return fallback
	}
	return value
}

func loadBoolDefault(key string, fallback bool) bool {
	raw := os.Getenv(key)
	if raw == "" {
		return fallback
	}
	switch strings.ToLower(strings.TrimSpace(raw)) {
	case "1", "true", "yes", "on":
		return true
	case "0", "false", "no", "off":
		return false
	default:
		return fallback
	}
}
