package config

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

const (
	defaultRedisURL               = "redis://localhost:6379"
	defaultGitWorkspacePath       = "./data/repos"
	defaultArtifactStorePath      = "./data/artifacts"
	defaultParserVersion          = "v0.1.0"
	defaultMongoURI               = "mongodb://localhost:27018/adpilot_code_parser"
	defaultMongoDatabase          = "adpilot_code_parser"
	defaultDocsMongoDatabase      = "repo_docs"
	defaultIndexingRunsDatabase   = "adpilot_repo_sync"
	defaultMongoArtifactMaxPartMB = 10
	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
	Git         GitConfig
	Parser      ParserConfig
	Stream      StreamConsumerConfig
}

// MongoConfig holds MongoDB and cross-database settings.
type MongoConfig struct {
	URI                  string
	Database             string
	DocsDatabase         string
	IndexingRunsDatabase string
	IndexingRunsEnabled  bool
	// ArtifactMaxPartBytes caps each stored graph chunk below MongoDB's 16 MB document limit.
	ArtifactMaxPartBytes int
}

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

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

// GitConfig holds settings for reading cloned repositories from Repo Sync workspace.
type GitConfig struct {
	WorkspacePath string
}

// EnsureWorkspace creates the git workspace directory when missing.
func (g GitConfig) EnsureWorkspace() error {
	path := strings.TrimSpace(g.WorkspacePath)
	if path == "" {
		return fmt.Errorf("git workspace path is required")
	}

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

// Ready reports whether the git workspace directory exists and is readable.
func (g GitConfig) Ready(ctx context.Context) bool {
	if err := ctx.Err(); err != nil {
		return false
	}

	path := strings.TrimSpace(g.WorkspacePath)
	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
	}

	dir, err := os.Open(absPath)
	if err != nil {
		return false
	}
	_ = dir.Close()
	return true
}

// ParserConfig holds parsing runtime settings.
type ParserConfig struct {
	Workers                   int
	Version                   string
	EnableFallback            bool
	CodeParseFailureThreshold float64
}

// 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
}

// 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 := loadParserWorkers()
	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"),
		},
		Mongo: MongoConfig{
			URI:                  loadWithLocalDefault("MONGO_URI", defaultMongoURI, environment),
			Database:             loadWithLocalDefault("MONGODB_DATABASE", defaultMongoDatabase, environment),
			DocsDatabase:         loadWithLocalDefault("DOCS_MONGODB_DATABASE", defaultDocsMongoDatabase, environment),
			IndexingRunsDatabase: loadWithLocalDefault("INDEXING_RUNS_DATABASE", defaultIndexingRunsDatabase, environment),
			IndexingRunsEnabled:  loadBoolDefault("INDEXING_RUNS_ENABLED", true),
			ArtifactMaxPartBytes: loadMongoArtifactMaxPartBytes(),
		},
		Stream: StreamConsumerConfig{
			Group:              getenv("REDIS_STREAM_GROUP", "code-parser-service"),
			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),
		},
		Git: GitConfig{
			WorkspacePath: loadWithLocalDefault("GIT_WORKSPACE_PATH", defaultGitWorkspacePath, environment),
		},
		Parser: ParserConfig{
			Workers:                   workers,
			Version:                   loadWithLocalDefault("PARSER_VERSION", defaultParserVersion, environment),
			EnableFallback:            loadBoolDefault("PARSER_ENABLE_FALLBACK", true),
			CodeParseFailureThreshold: loadFloatDefault("CODE_PARSE_FAILURE_THRESHOLD", 0.05),
		},
	}

	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},
		{"GIT_WORKSPACE_PATH", c.Git.WorkspacePath},
	}

	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 loadParserWorkers() (int, error) {
	raw := os.Getenv("PARSER_WORKERS")
	if raw == "" {
		return runtime.NumCPU(), nil
	}

	workers, err := strconv.Atoi(raw)
	if err != nil {
		return 0, fmt.Errorf("invalid PARSER_WORKERS: %w", err)
	}
	if workers < 1 {
		return 0, fmt.Errorf("invalid PARSER_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
	}
}

func loadMongoArtifactMaxPartBytes() int {
	mb := loadIntDefault("MONGO_ARTIFACT_MAX_PART_MB", defaultMongoArtifactMaxPartMB)
	if mb < 1 {
		mb = defaultMongoArtifactMaxPartMB
	}
	return mb * 1024 * 1024
}

func loadFloatDefault(key string, fallback float64) float64 {
	raw := os.Getenv(key)
	if raw == "" {
		return fallback
	}
	value, err := strconv.ParseFloat(raw, 64)
	if err != nil {
		return fallback
	}
	if value < 0 {
		return fallback
	}
	return value
}
