package db

import (
	"context"
	"encoding/json"
	"fmt"
	"log"
	"os"
	"path/filepath"

	"github.com/tidbcloud/controlplane9/internal/config"
)

// stateFileName is the name of the file that stores the provisioned instance state.
const stateFileName = "db-state.json"

// Provision creates a database instance using the configured provider.
// It creates per-service databases within the instance and writes state to disk.
func Provision(ctx context.Context, cfg *config.Config, services []string, stateDir string) (*ProvisionResult, error) {
	provider, err := providerFromConfig(cfg)
	if err != nil {
		return nil, err
	}
	return ProvisionWith(ctx, provider, services, stateDir)
}

// ProvisionWith creates a database instance using the given provider.
// Exported for testing with mock providers.
func ProvisionWith(ctx context.Context, provider Provider, services []string, stateDir string) (*ProvisionResult, error) {
	if provider == nil {
		return nil, fmt.Errorf("no database provider configured")
	}

	log.Printf("db: provisioning with %s...", provider.Name())

	inst, err := provider.Provision(ctx)
	if err != nil {
		return nil, fmt.Errorf("%s provisioning failed: %w", provider.Name(), err)
	}
	if inst == nil {
		return nil, fmt.Errorf("provider %s returned nil instance without error", provider.Name())
	}
	log.Printf("db: provisioned %s instance=%s host=%s:%d", provider.Name(), inst.ID, inst.Host, inst.Port)

	if provider.Name() == config.DBLocalMySQL {
		log.Printf("db: WARNING — using local MySQL, TiDB compatibility features unavailable")
	}

	// Validate service names before creating databases.
	for _, svc := range services {
		if err := ValidateServiceName(svc); err != nil {
			return nil, fmt.Errorf("invalid service name: %w", err)
		}
	}

	// Create per-service databases.
	var created []string
	for _, svc := range services {
		if err := provider.CreateDatabase(ctx, inst, svc); err != nil {
			// Best-effort cleanup: destroy the provisioned instance to avoid leaking resources.
			log.Printf("db: CreateDatabase failed for %q, attempting cleanup...", svc)
			if destroyErr := provider.Destroy(ctx, inst); destroyErr != nil {
				log.Printf("db: cleanup failed: %v", destroyErr)
			}
			return nil, fmt.Errorf("create database %q: %w", svc, err)
		}
		created = append(created, svc)
		log.Printf("db: created database %q", svc)
	}

	// Write state to disk for destroy.
	if err := writeState(stateDir, inst); err != nil {
		return nil, fmt.Errorf("write db state: %w", err)
	}

	return &ProvisionResult{
		Provider:  inst.Provider,
		ID:        inst.ID,
		Host:      inst.Host,
		Port:      inst.Port,
		Databases: created,
	}, nil
}

// Destroy tears down the database instance recorded in the state file.
func Destroy(ctx context.Context, stateDir string) (*DestroyResult, error) {
	inst, err := readState(stateDir)
	if err != nil {
		if os.IsNotExist(err) {
			return nil, fmt.Errorf("no database state found in %s — nothing to destroy", stateDir)
		}
		return nil, fmt.Errorf("read db state: %w", err)
	}

	provider, err := providerForInstance(inst)
	if err != nil {
		return nil, fmt.Errorf("resolve provider for destroy: %w", err)
	}
	log.Printf("db: destroying %s instance=%s", inst.Provider, inst.ID)

	if err := provider.Destroy(ctx, inst); err != nil {
		return nil, fmt.Errorf("destroy %s instance %s: %w", inst.Provider, inst.ID, err)
	}

	// Remove state file.
	statePath := filepath.Join(stateDir, stateFileName)
	if removeErr := os.Remove(statePath); removeErr != nil && !os.IsNotExist(removeErr) {
		log.Printf("db: warning: could not remove state file: %v", removeErr)
	}

	return &DestroyResult{
		Provider: inst.Provider,
		ID:       inst.ID,
	}, nil
}

// GenerateDSNs returns a map of service name → DSN for each service database.
func GenerateDSNs(stateDir string, services []string) (map[string]string, error) {
	inst, err := readState(stateDir)
	if err != nil {
		return nil, fmt.Errorf("read db state: %w", err)
	}

	dsns := make(map[string]string, len(services))
	for _, svc := range services {
		dsns[svc] = inst.DSN(svc)
	}
	return dsns, nil
}

// AdminDSN returns a DSN with no database selected, for administrative operations
// like DROP DATABASE / CREATE DATABASE.
func AdminDSN(stateDir string) (string, error) {
	inst, err := readState(stateDir)
	if err != nil {
		return "", fmt.Errorf("read db state: %w", err)
	}
	return inst.DSN(""), nil
}

// providerFromConfig returns the single active metadb provider for the session.
func providerFromConfig(cfg *config.Config) (Provider, error) {
	apiKey := os.Getenv("TIDB_CLOUD_API_KEY")

	// Convert AWS region to TiDB Cloud region format (e.g. "us-west-2" → "regions/aws-us-west-2").
	region := ""
	if cfg.Clouds.AWS.Region != "" {
		region = "regions/aws-" + cfg.Clouds.AWS.Region
	}

	switch cfg.Metadb.Provider {
	case config.DBTiDBCloudZero:
		return &ZeroProvider{NamePrefix: cfg.Name}, nil
	case config.DBTiDBCloudStarter:
		return &StarterProvider{NamePrefix: cfg.Name, APIKey: apiKey, Region: region}, nil
	case config.DBLocalMySQL:
		return &LocalMySQLProvider{}, nil
	default:
		return nil, fmt.Errorf("unsupported metadb provider %q", cfg.Metadb.Provider)
	}
}

// providerForInstance returns the appropriate provider for a previously provisioned instance.
func providerForInstance(inst *Instance) (Provider, error) {
	apiKey := os.Getenv("TIDB_CLOUD_API_KEY")

	switch inst.Provider {
	case config.DBTiDBCloudZero:
		return &ZeroProvider{}, nil
	case config.DBTiDBCloudStarter:
		return &StarterProvider{APIKey: apiKey}, nil
	case config.DBLocalMySQL:
		return &LocalMySQLProvider{}, nil
	default:
		return nil, fmt.Errorf("unsupported provider in state file: %q", inst.Provider)
	}
}

// writeState writes the instance state to disk for later destruction.
func writeState(dir string, inst *Instance) error {
	if err := os.MkdirAll(dir, 0o755); err != nil {
		return err
	}
	// State file stores connection details including password for destroy/DSN generation.
	// File permissions are 0o600 (owner-only read/write).
	data, err := json.MarshalIndent(inst, "", "  ") //nolint:gosec // G117: password stored in local state file with 0600 perms
	if err != nil {
		return err
	}
	return os.WriteFile(filepath.Join(dir, stateFileName), data, 0o600)
}

// readState reads the instance state from disk.
func readState(dir string) (*Instance, error) {
	data, err := os.ReadFile(filepath.Join(dir, stateFileName))
	if err != nil {
		return nil, err
	}
	var inst Instance
	if err := json.Unmarshal(data, &inst); err != nil {
		return nil, err
	}
	return &inst, nil
}
