package pack

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

type PublishProviderManifestRequest struct {
	RepoRoot      string
	PackID        string
	Manifest      ProviderManifest
	CommitMessage string
}

type PublishProviderManifestResult struct {
	RepoRoot          string `json:"repo_root"`
	PackID            string `json:"pack_id"`
	ProviderID        string `json:"provider_id"`
	ProviderPath      string `json:"provider_path"`
	PackVersionBefore string `json:"pack_version_before"`
	PackVersionAfter  string `json:"pack_version_after"`
	PublishMode       string `json:"publish_mode"`
	CommitMessage     string `json:"commit_message"`
	CommitSHA         string `json:"commit_sha"`
}

func PublishProviderManifest(ctx context.Context, req PublishProviderManifestRequest) (PublishProviderManifestResult, error) {
	repoRoot, err := resolveRepoRoot(req.RepoRoot)
	if err != nil {
		return PublishProviderManifestResult{}, err
	}

	manifest := normalizeProviderManifest(req.Manifest)
	packDir := filepath.Join(repoRoot, "packs", strings.TrimSpace(req.PackID))
	loadedPack, err := LoadDir(packDir)
	if err != nil {
		return PublishProviderManifestResult{}, fmt.Errorf("load pack dir %q: %w", packDir, err)
	}
	if err := validateProviders(packDir, []ProviderManifest{manifest}); err != nil {
		return PublishProviderManifestResult{}, err
	}

	providersDir := filepath.Join(packDir, loadedPack.Manifest.ProvidersDir)
	if err := os.MkdirAll(providersDir, 0o755); err != nil {
		return PublishProviderManifestResult{}, fmt.Errorf("ensure providers dir %q: %w", providersDir, err)
	}

	providerFileName := strings.TrimSpace(manifest.ProviderID) + ".json"
	providerPath := filepath.Join(providersDir, providerFileName)
	publishMode := "created"
	if _, err := os.Stat(providerPath); err == nil {
		publishMode = "updated"
	} else if !os.IsNotExist(err) {
		return PublishProviderManifestResult{}, fmt.Errorf("stat provider file %q: %w", providerPath, err)
	}

	providerBody, err := json.MarshalIndent(manifest, "", "  ")
	if err != nil {
		return PublishProviderManifestResult{}, fmt.Errorf("marshal provider manifest: %w", err)
	}
	providerBody = append(providerBody, '\n')
	if err := os.WriteFile(providerPath, providerBody, 0o644); err != nil {
		return PublishProviderManifestResult{}, fmt.Errorf("write provider manifest %q: %w", providerPath, err)
	}

	packManifest := loadedPack.Manifest
	previousVersion := packManifest.Version
	nextVersion, err := bumpPatchVersion(previousVersion)
	if err != nil {
		return PublishProviderManifestResult{}, err
	}
	packManifest.Version = nextVersion
	packBody, err := json.MarshalIndent(packManifest, "", "  ")
	if err != nil {
		return PublishProviderManifestResult{}, fmt.Errorf("marshal pack manifest: %w", err)
	}
	packBody = append(packBody, '\n')
	packManifestPath := filepath.Join(packDir, "pack.json")
	if err := os.WriteFile(packManifestPath, packBody, 0o644); err != nil {
		return PublishProviderManifestResult{}, fmt.Errorf("write pack manifest %q: %w", packManifestPath, err)
	}

	if err := updateChecksumFile(packDir, packManifest.ChecksumFile, []string{
		"pack.json",
		filepath.ToSlash(filepath.Join(packManifest.ProvidersDir, providerFileName)),
	}); err != nil {
		return PublishProviderManifestResult{}, err
	}

	if _, err := LoadDir(packDir); err != nil {
		return PublishProviderManifestResult{}, fmt.Errorf("re-validate published pack %q: %w", packDir, err)
	}

	commitMessage := strings.TrimSpace(req.CommitMessage)
	if commitMessage == "" {
		commitMessage = fmt.Sprintf("feat(pack): publish provider draft %s", manifest.ProviderID)
	}
	commitSHA, err := commitPackPublish(ctx, repoRoot, commitMessage, []string{
		filepath.Join("packs", packManifest.PackID, "pack.json"),
		filepath.Join("packs", packManifest.PackID, packManifest.ChecksumFile),
		filepath.Join("packs", packManifest.PackID, packManifest.ProvidersDir, providerFileName),
	})
	if err != nil {
		return PublishProviderManifestResult{}, err
	}

	return PublishProviderManifestResult{
		RepoRoot:          repoRoot,
		PackID:            packManifest.PackID,
		ProviderID:        manifest.ProviderID,
		ProviderPath:      filepath.ToSlash(filepath.Join("packs", packManifest.PackID, packManifest.ProvidersDir, providerFileName)),
		PackVersionBefore: previousVersion,
		PackVersionAfter:  nextVersion,
		PublishMode:       publishMode,
		CommitMessage:     commitMessage,
		CommitSHA:         commitSHA,
	}, nil
}

func normalizeProviderManifest(manifest ProviderManifest) ProviderManifest {
	normalized := manifest
	normalized.ProviderID = strings.TrimSpace(normalized.ProviderID)
	normalized.DisplayName = strings.TrimSpace(normalized.DisplayName)
	normalized.BaseURL = strings.TrimSpace(normalized.BaseURL)
	normalized.Platform = strings.TrimSpace(normalized.Platform)
	normalized.AccountType = strings.TrimSpace(normalized.AccountType)
	normalized.SmokeTestModel = strings.TrimSpace(normalized.SmokeTestModel)
	if normalized.AccountType == "" {
		normalized.AccountType = "apikey"
	}
	normalized.DefaultModels = normalizeModels(normalized.DefaultModels, normalized.SmokeTestModel)
	if normalized.GroupTemplate.Name == "" {
		normalized.GroupTemplate.Name = normalized.DisplayName + " 默认分组"
	}
	if normalized.GroupTemplate.RateMultiplier == 0 {
		normalized.GroupTemplate.RateMultiplier = 1.0
	}
	if normalized.ChannelTemplate.Name == "" {
		normalized.ChannelTemplate.Name = normalized.DisplayName + " 默认渠道"
	}
	if normalized.ChannelTemplate.ModelMapping == nil {
		normalized.ChannelTemplate.ModelMapping = make(map[string]string, len(normalized.DefaultModels))
	}
	for _, model := range normalized.DefaultModels {
		if _, ok := normalized.ChannelTemplate.ModelMapping[model]; !ok {
			normalized.ChannelTemplate.ModelMapping[model] = model
		}
	}
	if normalized.PlanTemplate.Name == "" {
		normalized.PlanTemplate.Name = normalized.DisplayName + " 默认套餐"
	}
	if normalized.PlanTemplate.ValidityDays <= 0 {
		normalized.PlanTemplate.ValidityDays = 30
	}
	if normalized.PlanTemplate.ValidityUnit == "" {
		normalized.PlanTemplate.ValidityUnit = "day"
	}
	if normalized.Import == (ImportOptions{}) {
		normalized.Import = ImportOptions{
			SupportsMultiKey: true,
			SupportsStrict:   true,
			SupportsPartial:  true,
		}
	}
	return normalized
}

func normalizeModels(models []string, smokeTestModel string) []string {
	normalized := make([]string, 0, len(models)+1)
	seen := make(map[string]struct{}, len(models)+1)
	for _, model := range models {
		model = strings.TrimSpace(model)
		if model == "" {
			continue
		}
		if _, ok := seen[model]; ok {
			continue
		}
		normalized = append(normalized, model)
		seen[model] = struct{}{}
	}
	smokeTestModel = strings.TrimSpace(smokeTestModel)
	if smokeTestModel != "" {
		if _, ok := seen[smokeTestModel]; !ok {
			normalized = append(normalized, smokeTestModel)
		}
	}
	return normalized
}

func resolveRepoRoot(repoRoot string) (string, error) {
	repoRoot = strings.TrimSpace(repoRoot)
	if repoRoot == "" {
		return "", fmt.Errorf("pack repo root is not configured")
	}
	absoluteRepoRoot, err := filepath.Abs(repoRoot)
	if err != nil {
		return "", fmt.Errorf("resolve repo root %q: %w", repoRoot, err)
	}
	info, err := os.Stat(absoluteRepoRoot)
	if err != nil {
		return "", fmt.Errorf("stat repo root %q: %w", absoluteRepoRoot, err)
	}
	if !info.IsDir() {
		return "", fmt.Errorf("repo root %q is not a directory", absoluteRepoRoot)
	}
	return absoluteRepoRoot, nil
}

func bumpPatchVersion(version string) (string, error) {
	parts := strings.Split(strings.TrimSpace(version), ".")
	if len(parts) != 3 {
		return "", fmt.Errorf("pack version %q must use x.y.z format", version)
	}
	patch, err := strconv.Atoi(parts[2])
	if err != nil {
		return "", fmt.Errorf("parse pack version %q patch: %w", version, err)
	}
	parts[2] = strconv.Itoa(patch + 1)
	return strings.Join(parts, "."), nil
}

func updateChecksumFile(packDir string, checksumFile string, relativePaths []string) error {
	path := filepath.Join(packDir, checksumFile)
	entries, err := readChecksumEntries(path)
	if err != nil {
		return err
	}
	for _, relativePath := range relativePaths {
		normalizedPath := filepath.ToSlash(filepath.Clean(strings.TrimSpace(relativePath)))
		if normalizedPath == "." || normalizedPath == "" {
			continue
		}
		body, err := os.ReadFile(filepath.Join(packDir, normalizedPath))
		if err != nil {
			return fmt.Errorf("read checksum target %q: %w", normalizedPath, err)
		}
		sum := sha256.Sum256(body)
		entries[normalizedPath] = hex.EncodeToString(sum[:])
	}
	paths := make([]string, 0, len(entries))
	for relativePath := range entries {
		paths = append(paths, relativePath)
	}
	sort.Strings(paths)
	lines := make([]string, 0, len(paths))
	for _, relativePath := range paths {
		lines = append(lines, fmt.Sprintf("%s  %s", entries[relativePath], relativePath))
	}
	if err := os.WriteFile(path, []byte(strings.Join(lines, "\n")+"\n"), 0o644); err != nil {
		return fmt.Errorf("write checksum file %q: %w", path, err)
	}
	return nil
}

func readChecksumEntries(path string) (map[string]string, error) {
	file, err := os.Open(path)
	if err != nil {
		return nil, fmt.Errorf("read checksum file %q: %w", path, err)
	}
	defer file.Close()

	entries := map[string]string{}
	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 nil, fmt.Errorf("checksum file %q line %d: invalid format", path, lineNumber)
		}
		entries[filepath.ToSlash(parts[1])] = parts[0]
	}
	if err := scanner.Err(); err != nil {
		return nil, fmt.Errorf("scan checksum file %q: %w", path, err)
	}
	return entries, nil
}

func commitPackPublish(ctx context.Context, repoRoot string, message string, relativePaths []string) (string, error) {
	addArgs := append([]string{"add"}, relativePaths...)
	if _, err := runGit(ctx, repoRoot, addArgs...); err != nil {
		return "", err
	}
	if _, err := runGit(ctx, repoRoot, "commit", "-m", message); err != nil {
		return "", err
	}
	sha, err := runGit(ctx, repoRoot, "rev-parse", "--short", "HEAD")
	if err != nil {
		return "", err
	}
	return strings.TrimSpace(sha), nil
}

func runGit(ctx context.Context, repoRoot string, args ...string) (string, error) {
	cmd := exec.CommandContext(ctx, "git", append([]string{"-C", repoRoot}, args...)...)
	output, err := cmd.CombinedOutput()
	if err != nil {
		return "", fmt.Errorf("run git %q: %w: %s", strings.Join(args, " "), err, strings.TrimSpace(string(output)))
	}
	return strings.TrimSpace(string(output)), nil
}
