package formula

import (
	"crypto/sha256"
	"embed"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"os"
	"path/filepath"
)

// Generate formulas directory from canonical source at .beads/formulas/
//go:generate sh -c "rm -rf formulas && mkdir -p formulas && cp ../../.beads/formulas/*.formula.toml formulas/"

//go:embed formulas/*.formula.toml
var formulasFS embed.FS

// InstalledRecord tracks which formulas were installed and their checksums.
// Stored in .beads/formulas/.installed.json
type InstalledRecord struct {
	Formulas map[string]string `json:"formulas"` // filename -> sha256 at install time
}

// FormulaStatus represents the status of a single formula during health check.
type FormulaStatus struct {
	Name          string
	Status        string // "ok", "outdated", "modified", "missing", "new", "untracked"
	EmbeddedHash  string // hash computed from embedded content
	InstalledHash string // hash we installed (from .installed.json)
	CurrentHash   string // hash of current file on disk
}

// HealthReport contains the results of checking formula health.
type HealthReport struct {
	Formulas []FormulaStatus
	// Counts
	OK        int
	Outdated  int // embedded changed, user hasn't modified
	Modified  int // user modified the file (tracked in .installed.json)
	Missing   int // file was deleted
	New       int // new formula not yet installed
	Untracked int // file exists but not in .installed.json (safe to update)
}

// computeHash computes SHA256 hash of data.
func computeHash(data []byte) string {
	hash := sha256.Sum256(data)
	return hex.EncodeToString(hash[:])
}

// getEmbeddedFormulas returns a map of filename -> sha256 for all embedded formulas.
func getEmbeddedFormulas() (map[string]string, error) {
	entries, err := formulasFS.ReadDir("formulas")
	if err != nil {
		return nil, fmt.Errorf("reading formulas directory: %w", err)
	}

	result := make(map[string]string)
	for _, entry := range entries {
		if entry.IsDir() {
			continue
		}
		content, err := formulasFS.ReadFile("formulas/" + entry.Name())
		if err != nil {
			return nil, fmt.Errorf("reading %s: %w", entry.Name(), err)
		}
		result[entry.Name()] = computeHash(content)
	}
	return result, nil
}

// loadInstalledRecord loads the installed record from disk.
func loadInstalledRecord(formulasDir string) (*InstalledRecord, error) {
	path := filepath.Join(formulasDir, ".installed.json")
	data, err := os.ReadFile(path)
	if os.IsNotExist(err) {
		return &InstalledRecord{Formulas: make(map[string]string)}, nil
	}
	if err != nil {
		return nil, fmt.Errorf("reading installed record: %w", err)
	}
	var r InstalledRecord
	if err := json.Unmarshal(data, &r); err != nil {
		return nil, fmt.Errorf("parsing installed record: %w", err)
	}
	if r.Formulas == nil {
		r.Formulas = make(map[string]string)
	}
	return &r, nil
}

// saveInstalledRecord saves the installed record to disk.
func saveInstalledRecord(formulasDir string, record *InstalledRecord) error {
	path := filepath.Join(formulasDir, ".installed.json")
	data, err := json.MarshalIndent(record, "", "  ")
	if err != nil {
		return fmt.Errorf("encoding installed record: %w", err)
	}
	return os.WriteFile(path, data, 0644)
}

// computeFileHash computes SHA256 hash of a file.
func computeFileHash(path string) (string, error) {
	data, err := os.ReadFile(path)
	if err != nil {
		return "", err
	}
	return computeHash(data), nil
}

// ProvisionFormulas creates the .beads/formulas/ directory with embedded formulas.
// This is called during gt install for fresh installations.
// If a formula already exists, it is skipped (no overwrite).
// Returns the number of formulas provisioned.
func ProvisionFormulas(beadsPath string) (int, error) {
	embedded, err := getEmbeddedFormulas()
	if err != nil {
		return 0, err
	}

	entries, err := formulasFS.ReadDir("formulas")
	if err != nil {
		return 0, fmt.Errorf("reading formulas directory: %w", err)
	}

	// Create .beads/formulas/ directory
	formulasDir := filepath.Join(beadsPath, ".beads", "formulas")
	if err := os.MkdirAll(formulasDir, 0755); err != nil {
		return 0, fmt.Errorf("creating formulas directory: %w", err)
	}

	// Load existing installed record (or create new)
	installed, err := loadInstalledRecord(formulasDir)
	if err != nil {
		return 0, err
	}

	count := 0
	for _, entry := range entries {
		if entry.IsDir() {
			continue
		}

		destPath := filepath.Join(formulasDir, entry.Name())

		// Skip if formula already exists (don't overwrite user customizations)
		if _, err := os.Stat(destPath); err == nil {
			continue
		} else if !os.IsNotExist(err) {
			// Log unexpected errors but continue
			continue
		}

		content, err := formulasFS.ReadFile("formulas/" + entry.Name())
		if err != nil {
			return count, fmt.Errorf("reading %s: %w", entry.Name(), err)
		}

		if err := os.WriteFile(destPath, content, 0644); err != nil {
			return count, fmt.Errorf("writing %s: %w", entry.Name(), err)
		}

		// Record the hash we installed
		if hash, ok := embedded[entry.Name()]; ok {
			installed.Formulas[entry.Name()] = hash
		}
		count++
	}

	// Save updated installed record
	if err := saveInstalledRecord(formulasDir, installed); err != nil {
		return count, fmt.Errorf("saving installed record: %w", err)
	}

	return count, nil
}

// CheckFormulaHealth checks the status of all formulas.
// Returns a report of which formulas are ok, outdated, modified, or missing.
func CheckFormulaHealth(beadsPath string) (*HealthReport, error) {
	embedded, err := getEmbeddedFormulas()
	if err != nil {
		return nil, err
	}

	formulasDir := filepath.Join(beadsPath, ".beads", "formulas")
	installed, err := loadInstalledRecord(formulasDir)
	if err != nil {
		return nil, err
	}

	report := &HealthReport{}

	for filename, embeddedHash := range embedded {
		status := FormulaStatus{
			Name:         filename,
			EmbeddedHash: embeddedHash,
		}

		installedHash, wasInstalled := installed.Formulas[filename]
		status.InstalledHash = installedHash

		destPath := filepath.Join(formulasDir, filename)
		currentHash, err := computeFileHash(destPath)

		if os.IsNotExist(err) {
			// File doesn't exist
			if wasInstalled {
				// We installed it before, user deleted it
				status.Status = "missing"
				report.Missing++
			} else {
				// New formula, never installed
				status.Status = "new"
				report.New++
			}
		} else if err != nil {
			// Some other error reading file
			status.Status = "error"
		} else {
			status.CurrentHash = currentHash

			if currentHash == embeddedHash {
				// File matches embedded - all good
				status.Status = "ok"
				report.OK++
			} else if wasInstalled && currentHash == installedHash {
				// File matches what we installed, but embedded has changed
				// User hasn't modified, safe to update
				status.Status = "outdated"
				report.Outdated++
			} else if wasInstalled {
				// File was tracked and user modified it - don't overwrite
				status.Status = "modified"
				report.Modified++
			} else {
				// File exists but not tracked (e.g., from older gt version)
				// Safe to update since we have no record of user modification
				status.Status = "untracked"
				report.Untracked++
			}
		}

		report.Formulas = append(report.Formulas, status)
	}

	return report, nil
}

// UpdateFormulas updates formulas that are safe to update (outdated, missing, or untracked).
// Skips user-modified formulas (tracked files that user changed).
// Returns counts of updated, skipped (modified), and reinstalled (missing).
func UpdateFormulas(beadsPath string) (updated, skipped, reinstalled int, err error) {
	embedded, err := getEmbeddedFormulas()
	if err != nil {
		return 0, 0, 0, err
	}

	formulasDir := filepath.Join(beadsPath, ".beads", "formulas")
	if err := os.MkdirAll(formulasDir, 0755); err != nil {
		return 0, 0, 0, fmt.Errorf("creating formulas directory: %w", err)
	}

	installed, err := loadInstalledRecord(formulasDir)
	if err != nil {
		return 0, 0, 0, err
	}

	for filename, embeddedHash := range embedded {
		installedHash, wasInstalled := installed.Formulas[filename]
		destPath := filepath.Join(formulasDir, filename)
		currentHash, fileErr := computeFileHash(destPath)

		shouldInstall := false
		isMissing := false
		isModified := false

		if os.IsNotExist(fileErr) {
			// File doesn't exist - install it
			shouldInstall = true
			if wasInstalled {
				isMissing = true
			}
		} else if fileErr != nil {
			// Error reading file, skip
			continue
		} else if currentHash == embeddedHash {
			// Already up to date
			continue
		} else if wasInstalled && currentHash == installedHash {
			// User hasn't modified, safe to update
			shouldInstall = true
		} else if wasInstalled {
			// Tracked file was modified by user - skip
			isModified = true
		} else {
			// Untracked file (e.g., from older gt version) - safe to update
			shouldInstall = true
		}

		if isModified {
			skipped++
			continue
		}

		if shouldInstall {
			content, err := formulasFS.ReadFile("formulas/" + filename)
			if err != nil {
				return updated, skipped, reinstalled, fmt.Errorf("reading %s: %w", filename, err)
			}

			if err := os.WriteFile(destPath, content, 0644); err != nil {
				return updated, skipped, reinstalled, fmt.Errorf("writing %s: %w", filename, err)
			}

			// Update installed record
			installed.Formulas[filename] = embeddedHash

			if isMissing {
				reinstalled++
			} else {
				updated++
			}
		}
	}

	// Save updated installed record
	if err := saveInstalledRecord(formulasDir, installed); err != nil {
		return updated, skipped, reinstalled, fmt.Errorf("saving installed record: %w", err)
	}

	return updated, skipped, reinstalled, nil
}
