package pack

import (
	"bufio"
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"os"
	"path/filepath"
	"sort"
	"strings"
)

const envAllowInsecureProviderBaseURL = "SUB2API_CRM_ALLOW_INSECURE_PROVIDER_BASE_URLS"

type Manifest struct {
	PackID         string `json:"pack_id"`
	Version        string `json:"version"`
	Vendor         string `json:"vendor"`
	TargetHost     string `json:"target_host"`
	MinHostVersion string `json:"min_host_version"`
	MaxHostVersion string `json:"max_host_version"`
	ProvidersDir   string `json:"providers_dir"`
	ChecksumFile   string `json:"checksum_file"`
}

type ProviderManifest struct {
	ProviderID                     string          `json:"provider_id"`
	DisplayName                    string          `json:"display_name"`
	BaseURL                        string          `json:"base_url"`
	Platform                       string          `json:"platform"`
	AccountType                    string          `json:"account_type"`
	ForceDisableOpenAIResponsesAPI bool            `json:"force_disable_openai_responses_api,omitempty"`
	HostOverlays                   []HostOverlay   `json:"host_overlays,omitempty"`
	DefaultModels                  []string        `json:"default_models"`
	SmokeTestModel                 string          `json:"smoke_test_model"`
	GroupTemplate                  GroupTemplate   `json:"group_template"`
	ChannelTemplate                ChannelTemplate `json:"channel_template"`
	PlanTemplate                   PlanTemplate    `json:"plan_template"`
	Import                         ImportOptions   `json:"import"`
}

type HostOverlay struct {
	OverlayID      string `json:"overlay_id"`
	DisplayName    string `json:"display_name"`
	TargetHost     string `json:"target_host"`
	MinHostVersion string `json:"min_host_version,omitempty"`
	MaxHostVersion string `json:"max_host_version,omitempty"`
	ApplyMode      string `json:"apply_mode,omitempty"`
	PatchPath      string `json:"patch_path,omitempty"`
	NotesPath      string `json:"notes_path,omitempty"`
	Reason         string `json:"reason"`
}

type GroupTemplate struct {
	Name           string  `json:"name"`
	RateMultiplier float64 `json:"rate_multiplier"`
}

type ChannelTemplate struct {
	Name         string            `json:"name"`
	ModelMapping map[string]string `json:"model_mapping"`
}

type PlanTemplate struct {
	Name         string  `json:"name"`
	Price        float64 `json:"price"`
	ValidityDays int     `json:"validity_days"`
	ValidityUnit string  `json:"validity_unit"`
}

type ImportOptions struct {
	SupportsMultiKey bool `json:"supports_multi_key"`
	SupportsStrict   bool `json:"supports_strict"`
	SupportsPartial  bool `json:"supports_partial"`
}

type LoadedPack struct {
	Dir       string
	Manifest  Manifest
	Providers []ProviderManifest
	Checksum  string
}

func LoadDir(dir string) (LoadedPack, error) {
	root := strings.TrimSpace(dir)
	if root == "" {
		return LoadedPack{}, fmt.Errorf("pack dir is required")
	}

	manifestPath := filepath.Join(root, "pack.json")
	manifestBytes, err := os.ReadFile(manifestPath)
	if err != nil {
		return LoadedPack{}, fmt.Errorf("read pack.json: %w", err)
	}

	var manifest Manifest
	if err := json.Unmarshal(manifestBytes, &manifest); err != nil {
		return LoadedPack{}, fmt.Errorf("decode pack.json: %w", err)
	}
	if err := validateManifest(manifest); err != nil {
		return LoadedPack{}, err
	}

	if err := validateChecksums(root, manifest.ChecksumFile); err != nil {
		return LoadedPack{}, err
	}

	providers, err := loadProviders(root, manifest.ProvidersDir)
	if err != nil {
		return LoadedPack{}, err
	}
	if len(providers) == 0 {
		return LoadedPack{}, fmt.Errorf("providers dir %q does not contain provider manifests", manifest.ProvidersDir)
	}
	if err := validateProviders(root, providers); err != nil {
		return LoadedPack{}, err
	}

	checksum, err := computeAggregateChecksum(root, manifest.ChecksumFile)
	if err != nil {
		return LoadedPack{}, err
	}

	return LoadedPack{Dir: root, Manifest: manifest, Providers: providers, Checksum: checksum}, nil
}

func validateManifest(manifest Manifest) error {
	switch {
	case strings.TrimSpace(manifest.PackID) == "":
		return fmt.Errorf("pack.json: pack_id is required")
	case strings.TrimSpace(manifest.Version) == "":
		return fmt.Errorf("pack.json: version is required")
	case strings.TrimSpace(manifest.TargetHost) == "":
		return fmt.Errorf("pack.json: target_host is required")
	case strings.TrimSpace(manifest.ProvidersDir) == "":
		return fmt.Errorf("pack.json: providers_dir is required")
	case strings.TrimSpace(manifest.ChecksumFile) == "":
		return fmt.Errorf("pack.json: checksum_file is required")
	}
	return nil
}

func loadProviders(root string, providersDir string) ([]ProviderManifest, error) {
	dir := filepath.Join(root, providersDir)
	entries, err := os.ReadDir(dir)
	if err != nil {
		return nil, fmt.Errorf("read providers dir %q: %w", providersDir, err)
	}

	providers := make([]ProviderManifest, 0, len(entries))
	for _, entry := range entries {
		if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".json") {
			continue
		}
		path := filepath.Join(dir, entry.Name())
		body, err := os.ReadFile(path)
		if err != nil {
			return nil, fmt.Errorf("read provider %q: %w", entry.Name(), err)
		}
		var provider ProviderManifest
		if err := json.Unmarshal(body, &provider); err != nil {
			return nil, fmt.Errorf("decode provider %q: %w", entry.Name(), err)
		}
		providers = append(providers, provider)
	}
	sort.Slice(providers, func(i, j int) bool { return providers[i].ProviderID < providers[j].ProviderID })
	return providers, nil
}

func validateProviders(root string, providers []ProviderManifest) error {
	seen := make(map[string]struct{}, len(providers))
	allowInsecureBaseURL := strings.EqualFold(strings.TrimSpace(os.Getenv(envAllowInsecureProviderBaseURL)), "1") ||
		strings.EqualFold(strings.TrimSpace(os.Getenv(envAllowInsecureProviderBaseURL)), "true")
	for _, provider := range providers {
		providerID := strings.TrimSpace(provider.ProviderID)
		missingDefaultModel := firstMissingDefaultModel(provider.DefaultModels, provider.ChannelTemplate.ModelMapping)
		switch {
		case providerID == "":
			return fmt.Errorf("provider manifest: provider_id is required")
		case strings.TrimSpace(provider.DisplayName) == "":
			return fmt.Errorf("provider %q: display_name is required", providerID)
		case !hasAllowedProviderBaseURL(strings.TrimSpace(provider.BaseURL), allowInsecureBaseURL):
			return fmt.Errorf("provider %q: base_url must use https", providerID)
		case strings.TrimSpace(provider.Platform) == "":
			return fmt.Errorf("provider %q: platform is required", providerID)
		case strings.TrimSpace(provider.AccountType) == "":
			return fmt.Errorf("provider %q: account_type is required", providerID)
		case len(provider.DefaultModels) == 0:
			return fmt.Errorf("provider %q: default_models must not be empty", providerID)
		case strings.TrimSpace(provider.SmokeTestModel) == "":
			return fmt.Errorf("provider %q: smoke_test_model is required", providerID)
		case !contains(provider.DefaultModels, provider.SmokeTestModel):
			return fmt.Errorf("provider %q: smoke_test_model must be present in default_models", providerID)
		case strings.TrimSpace(provider.GroupTemplate.Name) == "":
			return fmt.Errorf("provider %q: group_template.name is required", providerID)
		case strings.TrimSpace(provider.ChannelTemplate.Name) == "":
			return fmt.Errorf("provider %q: channel_template.name is required", providerID)
		case len(provider.ChannelTemplate.ModelMapping) == 0:
			return fmt.Errorf("provider %q: channel_template.model_mapping must not be empty", providerID)
		case !containsProviderModel(provider.ChannelTemplate.ModelMapping, provider.SmokeTestModel):
			return fmt.Errorf("provider %q: channel_template.model_mapping must include smoke_test_model %q", providerID, provider.SmokeTestModel)
		case missingDefaultModel != "":
			return fmt.Errorf("provider %q: channel_template.model_mapping must cover default_models, missing %q", providerID, missingDefaultModel)
		case strings.TrimSpace(provider.PlanTemplate.Name) == "":
			return fmt.Errorf("provider %q: plan_template.name is required", providerID)
		case provider.PlanTemplate.ValidityDays <= 0:
			return fmt.Errorf("provider %q: plan_template.validity_days must be positive", providerID)
		}
		if _, ok := seen[providerID]; ok {
			return fmt.Errorf("duplicate provider_id %q", providerID)
		}
		if err := validateHostOverlays(root, provider); err != nil {
			return err
		}
		seen[providerID] = struct{}{}
	}
	return nil
}

func validateHostOverlays(root string, provider ProviderManifest) error {
	if len(provider.HostOverlays) == 0 {
		return nil
	}
	seen := make(map[string]struct{}, len(provider.HostOverlays))
	for _, overlay := range provider.HostOverlays {
		overlayID := strings.TrimSpace(overlay.OverlayID)
		switch {
		case overlayID == "":
			return fmt.Errorf("provider %q: host_overlays.overlay_id is required", provider.ProviderID)
		case strings.TrimSpace(overlay.DisplayName) == "":
			return fmt.Errorf("provider %q overlay %q: display_name is required", provider.ProviderID, overlayID)
		case strings.TrimSpace(overlay.TargetHost) == "":
			return fmt.Errorf("provider %q overlay %q: target_host is required", provider.ProviderID, overlayID)
		case strings.TrimSpace(overlay.Reason) == "":
			return fmt.Errorf("provider %q overlay %q: reason is required", provider.ProviderID, overlayID)
		}
		if _, ok := seen[overlayID]; ok {
			return fmt.Errorf("provider %q: duplicate host overlay %q", provider.ProviderID, overlayID)
		}
		if err := validateHostOverlayVersionRange(overlay); err != nil {
			return fmt.Errorf("provider %q overlay %q: %w", provider.ProviderID, overlayID, err)
		}
		if err := validateOverlayFileRef(root, overlay.PatchPath); err != nil {
			return fmt.Errorf("provider %q overlay %q: %w", provider.ProviderID, overlayID, err)
		}
		if err := validateOverlayFileRef(root, overlay.NotesPath); err != nil {
			return fmt.Errorf("provider %q overlay %q: %w", provider.ProviderID, overlayID, err)
		}
		seen[overlayID] = struct{}{}
	}
	return nil
}

func validateHostOverlayVersionRange(overlay HostOverlay) error {
	minVersion := strings.TrimSpace(overlay.MinHostVersion)
	if minVersion != "" {
		if _, err := parseVersion(minVersion); err != nil {
			return fmt.Errorf("parse min_host_version: %w", err)
		}
	}
	maxVersion := strings.TrimSpace(overlay.MaxHostVersion)
	if maxVersion != "" {
		if strings.HasSuffix(normalizeVersion(maxVersion), ".x") {
			if _, err := matchesMaxConstraint("0.0.0", maxVersion); err != nil {
				return fmt.Errorf("parse max_host_version: %w", err)
			}
		} else if _, err := parseVersion(maxVersion); err != nil {
			return fmt.Errorf("parse max_host_version: %w", err)
		}
	}
	if minVersion != "" && maxVersion != "" && !strings.HasSuffix(normalizeVersion(maxVersion), ".x") {
		cmp, err := compareVersions(minVersion, maxVersion)
		if err != nil {
			return fmt.Errorf("compare version range: %w", err)
		}
		if cmp > 0 {
			return fmt.Errorf("min_host_version %q is above max_host_version %q", minVersion, maxVersion)
		}
	}
	return nil
}

func validateOverlayFileRef(root string, relativePath string) error {
	trimmed := strings.TrimSpace(relativePath)
	if trimmed == "" {
		return nil
	}
	if filepath.IsAbs(trimmed) {
		return fmt.Errorf("file reference %q must be relative to pack root", trimmed)
	}
	if _, err := os.Stat(filepath.Join(root, trimmed)); err != nil {
		return fmt.Errorf("read file reference %q: %w", trimmed, err)
	}
	return nil
}

func hasAllowedProviderBaseURL(baseURL string, allowInsecureBaseURL bool) bool {
	if strings.HasPrefix(baseURL, "https://") {
		return true
	}
	return allowInsecureBaseURL && strings.HasPrefix(baseURL, "http://")
}

func validateChecksums(root string, checksumFile string) error {
	path := filepath.Join(root, checksumFile)
	file, err := os.Open(path)
	if err != nil {
		return fmt.Errorf("read checksum file %q: %w", checksumFile, err)
	}
	defer file.Close()

	scanner := bufio.NewScanner(file)
	lineNumber := 0
	for scanner.Scan() {
		lineNumber++
		line := strings.TrimSpace(scanner.Text())
		if line == "" {
			continue
		}
		parts := strings.Fields(line)
		if len(parts) != 2 {
			return fmt.Errorf("checksum file %q line %d: invalid format", checksumFile, lineNumber)
		}
		relativePath := parts[1]
		body, err := os.ReadFile(filepath.Join(root, relativePath))
		if err != nil {
			return fmt.Errorf("checksum file %q line %d: read %q: %w", checksumFile, lineNumber, relativePath, err)
		}
		sum := sha256.Sum256(body)
		actual := hex.EncodeToString(sum[:])
		if !strings.EqualFold(parts[0], actual) {
			return fmt.Errorf("checksum mismatch for %s", relativePath)
		}
	}
	if err := scanner.Err(); err != nil {
		return fmt.Errorf("scan checksum file %q: %w", checksumFile, err)
	}
	return nil
}

func computeAggregateChecksum(root string, checksumFile string) (string, error) {
	body, err := os.ReadFile(filepath.Join(root, checksumFile))
	if err != nil {
		return "", fmt.Errorf("read checksum file %q: %w", checksumFile, err)
	}
	sum := sha256.Sum256(body)
	return hex.EncodeToString(sum[:]), nil
}

func contains(items []string, target string) bool {
	for _, item := range items {
		if strings.TrimSpace(item) == strings.TrimSpace(target) {
			return true
		}
	}
	return false
}

func containsProviderModel(modelMapping map[string]string, target string) bool {
	trimmedTarget := strings.TrimSpace(target)
	if trimmedTarget == "" {
		return false
	}
	for sourceModel, mappedModel := range modelMapping {
		if strings.TrimSpace(sourceModel) == trimmedTarget || strings.TrimSpace(mappedModel) == trimmedTarget {
			return true
		}
	}
	return false
}

func firstMissingDefaultModel(defaultModels []string, modelMapping map[string]string) string {
	for _, model := range defaultModels {
		trimmedModel := strings.TrimSpace(model)
		if trimmedModel == "" {
			continue
		}
		if !containsProviderModel(modelMapping, trimmedModel) {
			return trimmedModel
		}
	}
	return ""
}
