package infra

import (
	"fmt"
	"os"
	"path/filepath"
	"strings"
	"time"
)

// VaultDoctorEntry holds the health report for a single vault.
type VaultDoctorEntry struct {
	VaultName     string   `json:"vault_name"`
	VaultPath     string   `json:"vault_path"`
	ProjectID     string   `json:"project_id"`
	Issues        []string `json:"issues"`         // human-readable issues; empty = healthy
	IndexedFiles  int      `json:"indexed_files"`  // 0 if no vault_index.db
	LastIndexedAt int64    `json:"last_indexed_at"` // unix seconds; 0 if N/A
	DiskFiles     int      `json:"disk_files"`     // count via WalkDir (no hashing)
	Status        string   `json:"status"`         // "ok" | "warning" | "error"
}

// VaultDoctor audits every vault declared in projects/*/vaults/vault.yaml under
// repoRoot. For each vault it performs a series of checks (disk presence, layout,
// index existence, staleness, drift) and returns a slice of VaultDoctorEntry.
//
// The function is read-only: it never writes to disk or any database.
// Returns an error only if VaultManifestRead fails (manifest parse error).
func VaultDoctor(repoRoot string) ([]VaultDoctorEntry, error) {
	entries, err := VaultManifestRead(repoRoot)
	if err != nil {
		return nil, fmt.Errorf("vault_doctor: read manifests: %w", err)
	}

	results := make([]VaultDoctorEntry, 0, len(entries))
	for _, e := range entries {
		result := auditVault(e)
		results = append(results, result)
	}
	return results, nil
}

func auditVault(e VaultManifestEntry) VaultDoctorEntry {
	entry := VaultDoctorEntry{
		VaultName: e.Name,
		VaultPath: e.Path,
		ProjectID: e.ProjectID,
	}

	// Resolve symlinks for disk checks
	realPath, err := filepath.EvalSymlinks(e.Path)
	if err != nil || realPath == "" {
		realPath = e.Path
	}

	// CHECK 1: directory_missing
	info, statErr := os.Stat(realPath)
	if statErr != nil || !info.IsDir() {
		entry.Issues = append(entry.Issues, "directory_missing")
		entry.Status = "error"
		return entry
	}

	// COUNT disk files (cheap walk — no hashing, no mime detection)
	diskCount := countDiskFiles(realPath)
	entry.DiskFiles = diskCount

	// CHECK 2: layout_missing / non_standard_layout
	hasData := dirExists(filepath.Join(realPath, "data"))
	hasKnowledge := dirExists(filepath.Join(realPath, "knowledge"))
	if !hasData && !hasKnowledge {
		// Check if it looks like a non-standard but intentional layout
		if hasNonStandardLayout(realPath) {
			entry.Issues = append(entry.Issues, "non_standard_layout")
		} else {
			entry.Issues = append(entry.Issues, "layout_missing")
		}
	}

	// CHECK 3: index_missing
	indexPath := filepath.Join(realPath, "vault_index.db")
	_, indexStatErr := os.Stat(indexPath)
	if indexStatErr != nil {
		entry.Issues = append(entry.Issues, "index_missing")
		entry.setWarningStatus()
		entry.setFinalStatus()
		return entry
	}

	// Open vault index (read-only) for checks 4 and 5
	vdb, openErr := VaultIndexOpen(realPath)
	if openErr != nil {
		entry.Issues = append(entry.Issues, fmt.Sprintf("index_open_error: %v", openErr))
		entry.setWarningStatus()
		return entry
	}
	defer vdb.Close()

	// Query indexed file count and max indexed_at
	var indexedCount int
	var maxIndexedAt int64
	row := vdb.QueryRow(`SELECT COUNT(*), COALESCE(MAX(indexed_at), 0) FROM files`)
	if scanErr := row.Scan(&indexedCount, &maxIndexedAt); scanErr != nil {
		entry.Issues = append(entry.Issues, fmt.Sprintf("index_query_error: %v", scanErr))
	} else {
		entry.IndexedFiles = indexedCount
		entry.LastIndexedAt = maxIndexedAt
	}

	// CHECK 4: index_stale — any file on disk newer than MAX(indexed_at)
	if maxIndexedAt > 0 {
		maxTime := time.Unix(maxIndexedAt, 0)
		if isIndexStale(realPath, maxTime) {
			entry.Issues = append(entry.Issues, "index_stale")
		}
	}

	// CHECK 5: index_drift — disk file count != indexed count
	if indexedCount != diskCount {
		entry.Issues = append(entry.Issues, fmt.Sprintf("index_drift: disk=%d indexed=%d", diskCount, indexedCount))
	}

	// CHECK 6: empty_vault
	if diskCount == 0 {
		entry.Issues = append(entry.Issues, "empty_vault")
	}

	entry.setFinalStatus()
	return entry
}

// setWarningStatus sets status to warning if not already error.
func (e *VaultDoctorEntry) setWarningStatus() {
	if e.Status != "error" {
		e.Status = "warning"
	}
}

// setFinalStatus derives the final Status from Issues.
func (e *VaultDoctorEntry) setFinalStatus() {
	if e.Status == "error" {
		return
	}
	if len(e.Issues) == 0 {
		e.Status = "ok"
	} else {
		e.Status = "warning"
	}
}

// countDiskFiles walks realPath and counts regular files, excluding:
// vault_index.db*, .git/, hidden files/dirs at any depth.
func countDiskFiles(realPath string) int {
	count := 0
	_ = filepath.WalkDir(realPath, func(path string, d os.DirEntry, err error) error {
		if err != nil {
			return nil
		}
		name := d.Name()
		// Skip hidden entries
		if strings.HasPrefix(name, ".") {
			if d.IsDir() {
				return filepath.SkipDir
			}
			return nil
		}
		// Skip .git
		if d.IsDir() && name == ".git" {
			return filepath.SkipDir
		}
		// Skip vault_index.db files
		if !d.IsDir() && (name == "vault_index.db" || name == "vault_index.db-shm" || name == "vault_index.db-wal") {
			return nil
		}
		if !d.IsDir() {
			count++
		}
		return nil
	})
	return count
}

// isIndexStale returns true if any regular file under realPath has an mtime
// strictly after maxTime (excluding vault_index.db* and hidden files).
func isIndexStale(realPath string, maxTime time.Time) bool {
	stale := false
	_ = filepath.WalkDir(realPath, func(path string, d os.DirEntry, err error) error {
		if err != nil || stale {
			return nil
		}
		name := d.Name()
		if strings.HasPrefix(name, ".") {
			if d.IsDir() {
				return filepath.SkipDir
			}
			return nil
		}
		if d.IsDir() && name == ".git" {
			return filepath.SkipDir
		}
		if !d.IsDir() {
			if name == "vault_index.db" || name == "vault_index.db-shm" || name == "vault_index.db-wal" {
				return nil
			}
			fi, statErr := d.Info()
			if statErr == nil && fi.ModTime().After(maxTime) {
				stale = true
			}
		}
		return nil
	})
	return stale
}

// hasNonStandardLayout returns true when a vault directory contains
// subdirectories that are clearly intentional but not data/knowledge.
// Heuristic: any subdir at the vault root that is not data/knowledge.
func hasNonStandardLayout(realPath string) bool {
	entries, err := os.ReadDir(realPath)
	if err != nil {
		return false
	}
	standardDirs := map[string]bool{"data": true, "knowledge": true, ".git": true}
	for _, e := range entries {
		if e.IsDir() && !standardDirs[e.Name()] && !strings.HasPrefix(e.Name(), ".") {
			return true
		}
	}
	return false
}
