// Package doctor provides health check and repair functionality for beads.
package doctor

import (
	"database/sql"
	"encoding/json"
	"fmt"
	"os"
	"path/filepath"
	"strings"

	_ "github.com/ncruces/go-sqlite3/driver"
	_ "github.com/ncruces/go-sqlite3/embed"
	"github.com/steveyegge/beads/internal/beads"
	"github.com/steveyegge/beads/internal/configfile"
	"github.com/steveyegge/beads/internal/types"
)

// DeepValidationResult holds all deep validation check results
type DeepValidationResult struct {
	ParentConsistency    DoctorCheck   `json:"parent_consistency"`
	DependencyIntegrity  DoctorCheck   `json:"dependency_integrity"`
	EpicCompleteness     DoctorCheck   `json:"epic_completeness"`
	AgentBeadIntegrity   DoctorCheck   `json:"agent_bead_integrity"`
	MailThreadIntegrity  DoctorCheck   `json:"mail_thread_integrity"`
	MoleculeIntegrity    DoctorCheck   `json:"molecule_integrity"`
	AllChecks            []DoctorCheck `json:"all_checks"`
	TotalIssues          int           `json:"total_issues"`
	TotalDependencies    int           `json:"total_dependencies"`
	OverallOK            bool          `json:"overall_ok"`
}

// RunDeepValidation runs all deep validation checks on the issue graph.
// This may be slow on large databases.
func RunDeepValidation(path string) DeepValidationResult {
	result := DeepValidationResult{
		OverallOK: true,
	}

	// Follow redirect to resolve actual beads directory
	beadsDir := resolveBeadsDir(filepath.Join(path, ".beads"))

	// Get database path
	var dbPath string
	if cfg, err := configfile.Load(beadsDir); err == nil && cfg != nil && cfg.Database != "" {
		dbPath = cfg.DatabasePath(beadsDir)
	} else {
		dbPath = filepath.Join(beadsDir, beads.CanonicalDatabaseName)
	}

	// Skip if database doesn't exist
	if _, err := os.Stat(dbPath); os.IsNotExist(err) {
		check := DoctorCheck{
			Name:     "Deep Validation",
			Status:   StatusOK,
			Message:  "N/A (no database)",
			Category: CategoryMaintenance,
		}
		result.AllChecks = append(result.AllChecks, check)
		return result
	}

	// Open database
	db, err := sql.Open("sqlite3", sqliteConnString(dbPath, true))
	if err != nil {
		check := DoctorCheck{
			Name:     "Deep Validation",
			Status:   StatusError,
			Message:  "Unable to open database",
			Detail:   err.Error(),
			Category: CategoryMaintenance,
		}
		result.AllChecks = append(result.AllChecks, check)
		result.OverallOK = false
		return result
	}
	defer db.Close()

	// Get counts for progress reporting
	_ = db.QueryRow("SELECT COUNT(*) FROM issues WHERE status != 'tombstone'").Scan(&result.TotalIssues)
	_ = db.QueryRow("SELECT COUNT(*) FROM dependencies").Scan(&result.TotalDependencies)

	// Run all deep checks
	result.ParentConsistency = checkParentConsistency(db)
	result.AllChecks = append(result.AllChecks, result.ParentConsistency)
	if result.ParentConsistency.Status == StatusError {
		result.OverallOK = false
	}

	result.DependencyIntegrity = checkDependencyIntegrity(db)
	result.AllChecks = append(result.AllChecks, result.DependencyIntegrity)
	if result.DependencyIntegrity.Status == StatusError {
		result.OverallOK = false
	}

	result.EpicCompleteness = checkEpicCompleteness(db)
	result.AllChecks = append(result.AllChecks, result.EpicCompleteness)
	if result.EpicCompleteness.Status == StatusWarning {
		// Epic completeness is informational, not an error
	}

	result.AgentBeadIntegrity = checkAgentBeadIntegrity(db)
	result.AllChecks = append(result.AllChecks, result.AgentBeadIntegrity)
	if result.AgentBeadIntegrity.Status == StatusError {
		result.OverallOK = false
	}

	result.MailThreadIntegrity = checkMailThreadIntegrity(db)
	result.AllChecks = append(result.AllChecks, result.MailThreadIntegrity)
	if result.MailThreadIntegrity.Status == StatusError {
		result.OverallOK = false
	}

	result.MoleculeIntegrity = checkMoleculeIntegrity(db)
	result.AllChecks = append(result.AllChecks, result.MoleculeIntegrity)
	if result.MoleculeIntegrity.Status == StatusError {
		result.OverallOK = false
	}

	return result
}

// checkParentConsistency verifies that all parent-child dependencies point to existing issues
func checkParentConsistency(db *sql.DB) DoctorCheck {
	check := DoctorCheck{
		Name:     "Parent Consistency",
		Category: CategoryMetadata,
	}

	// Find parent-child deps where either side doesn't exist or is a tombstone
	query := `
		SELECT d.issue_id, d.depends_on_id
		FROM dependencies d
		WHERE d.type = 'parent-child'
		  AND (
		    NOT EXISTS (SELECT 1 FROM issues WHERE id = d.issue_id AND status != 'tombstone')
		    OR NOT EXISTS (SELECT 1 FROM issues WHERE id = d.depends_on_id AND status != 'tombstone')
		  )
		LIMIT 10`

	rows, err := db.Query(query)
	if err != nil {
		check.Status = StatusWarning
		check.Message = "Unable to check parent consistency"
		check.Detail = err.Error()
		return check
	}
	defer rows.Close()

	var orphanedDeps []string
	for rows.Next() {
		var issueID, parentID string
		if err := rows.Scan(&issueID, &parentID); err == nil {
			orphanedDeps = append(orphanedDeps, fmt.Sprintf("%s→%s", issueID, parentID))
		}
	}

	if len(orphanedDeps) == 0 {
		check.Status = StatusOK
		check.Message = "All parent-child relationships valid"
		return check
	}

	check.Status = StatusError
	check.Message = fmt.Sprintf("Found %d orphaned parent-child dependencies", len(orphanedDeps))
	check.Detail = fmt.Sprintf("Examples: %s", strings.Join(orphanedDeps[:min(3, len(orphanedDeps))], ", "))
	check.Fix = "Run 'bd doctor --fix' to remove orphaned dependencies"
	return check
}

// checkDependencyIntegrity verifies that all dependencies point to existing issues
func checkDependencyIntegrity(db *sql.DB) DoctorCheck {
	check := DoctorCheck{
		Name:     "Dependency Integrity",
		Category: CategoryMetadata,
	}

	// Find any deps where either side is missing (excluding tombstones from check)
	query := `
		SELECT d.issue_id, d.depends_on_id, d.type
		FROM dependencies d
		WHERE (
		    NOT EXISTS (SELECT 1 FROM issues WHERE id = d.issue_id)
		    OR NOT EXISTS (SELECT 1 FROM issues WHERE id = d.depends_on_id)
		  )
		LIMIT 10`

	rows, err := db.Query(query)
	if err != nil {
		check.Status = StatusWarning
		check.Message = "Unable to check dependency integrity"
		check.Detail = err.Error()
		return check
	}
	defer rows.Close()

	var brokenDeps []string
	for rows.Next() {
		var issueID, dependsOnID, depType string
		if err := rows.Scan(&issueID, &dependsOnID, &depType); err == nil {
			brokenDeps = append(brokenDeps, fmt.Sprintf("%s→%s (%s)", issueID, dependsOnID, depType))
		}
	}

	if len(brokenDeps) == 0 {
		check.Status = StatusOK
		check.Message = "All dependencies point to existing issues"
		return check
	}

	check.Status = StatusError
	check.Message = fmt.Sprintf("Found %d broken dependencies", len(brokenDeps))
	check.Detail = fmt.Sprintf("Examples: %s", strings.Join(brokenDeps[:min(3, len(brokenDeps))], ", "))
	check.Fix = "Run 'bd repair-deps' to remove broken dependencies"
	return check
}

// checkEpicCompleteness finds epics that could be closed (all children closed)
func checkEpicCompleteness(db *sql.DB) DoctorCheck {
	check := DoctorCheck{
		Name:     "Epic Completeness",
		Category: CategoryMetadata,
	}

	// Find epics where all children are closed but epic is still open
	query := `
		SELECT e.id, e.title,
		       COUNT(c.id) as total_children,
		       SUM(CASE WHEN c.status = 'closed' THEN 1 ELSE 0 END) as closed_children
		FROM issues e
		JOIN dependencies d ON d.depends_on_id = e.id AND d.type = 'parent-child'
		JOIN issues c ON c.id = d.issue_id AND c.status != 'tombstone'
		WHERE e.issue_type = 'epic'
		  AND e.status NOT IN ('closed', 'tombstone')
		GROUP BY e.id
		HAVING total_children > 0 AND total_children = closed_children
		LIMIT 20`

	rows, err := db.Query(query)
	if err != nil {
		check.Status = StatusWarning
		check.Message = "Unable to check epic completeness"
		check.Detail = err.Error()
		return check
	}
	defer rows.Close()

	var completedEpics []string
	for rows.Next() {
		var id, title string
		var total, closed int
		if err := rows.Scan(&id, &title, &total, &closed); err == nil {
			completedEpics = append(completedEpics, fmt.Sprintf("%s (%d/%d)", id, closed, total))
		}
	}

	if len(completedEpics) == 0 {
		check.Status = StatusOK
		check.Message = "No epics eligible for closure"
		return check
	}

	check.Status = StatusWarning
	check.Message = fmt.Sprintf("Found %d epics ready to close", len(completedEpics))
	check.Detail = fmt.Sprintf("Examples: %s", strings.Join(completedEpics[:min(5, len(completedEpics))], ", "))
	check.Fix = "Run 'bd close <epic-id>' to close completed epics"
	return check
}

// agentBeadInfo holds info about an agent bead for integrity checking
type agentBeadInfo struct {
	ID         string
	Title      string
	RoleBead   string
	AgentState string
	RoleType   string
}

// checkAgentBeadIntegrity verifies that agent beads have required fields
func checkAgentBeadIntegrity(db *sql.DB) DoctorCheck {
	check := DoctorCheck{
		Name:     "Agent Bead Integrity",
		Category: CategoryMetadata,
	}

	// Check if agent bead columns exist (may not in older schemas)
	var hasColumns bool
	err := db.QueryRow(`
		SELECT COUNT(*) > 0 FROM pragma_table_info('issues')
		WHERE name IN ('role_bead', 'agent_state', 'role_type')
	`).Scan(&hasColumns)
	if err != nil || !hasColumns {
		check.Status = StatusOK
		check.Message = "N/A (schema doesn't support agent beads)"
		return check
	}

	// Find agent beads missing required role_bead
	// Note: We query JSON metadata from notes field or check for role_bead column
	query := `
		SELECT id, title,
		       COALESCE(json_extract(notes, '$.role_bead'), '') as role_bead,
		       COALESCE(json_extract(notes, '$.agent_state'), '') as agent_state,
		       COALESCE(json_extract(notes, '$.role_type'), '') as role_type
		FROM issues
		WHERE issue_type = 'agent'
		  AND status != 'tombstone'
		LIMIT 100`

	rows, err := db.Query(query)
	if err != nil {
		// Try alternate approach without JSON extraction
		query = `
			SELECT id, title, '', '', ''
			FROM issues
			WHERE issue_type = 'agent'
			  AND status != 'tombstone'
			LIMIT 100`
		rows, err = db.Query(query)
		if err != nil {
			check.Status = StatusOK
			check.Message = "No agent beads found"
			return check
		}
	}
	defer rows.Close()

	var agents []agentBeadInfo
	var invalidAgents []string

	for rows.Next() {
		var agent agentBeadInfo
		if err := rows.Scan(&agent.ID, &agent.Title, &agent.RoleBead, &agent.AgentState, &agent.RoleType); err == nil {
			agents = append(agents, agent)

			// Validate agent state
			if agent.AgentState != "" {
				state := types.AgentState(agent.AgentState)
				if !state.IsValid() {
					invalidAgents = append(invalidAgents, fmt.Sprintf("%s (invalid state: %s)", agent.ID, agent.AgentState))
				}
			}
		}
	}

	if len(agents) == 0 {
		check.Status = StatusOK
		check.Message = "No agent beads to validate"
		return check
	}

	if len(invalidAgents) > 0 {
		check.Status = StatusError
		check.Message = fmt.Sprintf("Found %d agents with invalid data", len(invalidAgents))
		check.Detail = fmt.Sprintf("Examples: %s", strings.Join(invalidAgents[:min(3, len(invalidAgents))], ", "))
		check.Fix = "Update agent beads with valid agent_state values"
		return check
	}

	check.Status = StatusOK
	check.Message = fmt.Sprintf("%d agent beads validated", len(agents))
	return check
}

// checkMailThreadIntegrity verifies that mail thread_id references are valid
func checkMailThreadIntegrity(db *sql.DB) DoctorCheck {
	check := DoctorCheck{
		Name:     "Mail Thread Integrity",
		Category: CategoryMetadata,
	}

	// Check if thread_id column exists
	var hasThreadID bool
	err := db.QueryRow(`
		SELECT COUNT(*) > 0 FROM pragma_table_info('dependencies')
		WHERE name = 'thread_id'
	`).Scan(&hasThreadID)
	if err != nil || !hasThreadID {
		check.Status = StatusOK
		check.Message = "N/A (schema doesn't support thread_id)"
		return check
	}

	// Find thread_ids that don't point to existing issues
	query := `
		SELECT d.thread_id, COUNT(*) as refs
		FROM dependencies d
		WHERE d.thread_id != ''
		  AND d.thread_id IS NOT NULL
		  AND NOT EXISTS (SELECT 1 FROM issues WHERE id = d.thread_id)
		GROUP BY d.thread_id
		LIMIT 10`

	rows, err := db.Query(query)
	if err != nil {
		check.Status = StatusWarning
		check.Message = "Unable to check thread integrity"
		check.Detail = err.Error()
		return check
	}
	defer rows.Close()

	var orphanedThreads []string
	totalOrphaned := 0
	for rows.Next() {
		var threadID string
		var refs int
		if err := rows.Scan(&threadID, &refs); err == nil {
			orphanedThreads = append(orphanedThreads, fmt.Sprintf("%s (%d refs)", threadID, refs))
			totalOrphaned += refs
		}
	}

	if len(orphanedThreads) == 0 {
		check.Status = StatusOK
		check.Message = "All thread references valid"
		return check
	}

	check.Status = StatusWarning
	check.Message = fmt.Sprintf("Found %d orphaned thread references", totalOrphaned)
	check.Detail = fmt.Sprintf("Threads: %s", strings.Join(orphanedThreads[:min(3, len(orphanedThreads))], ", "))
	check.Fix = "Clear orphaned thread_id values in dependencies"
	return check
}

// moleculeInfo holds info about a molecule for integrity checking
type moleculeInfo struct {
	ID         string
	Title      string
	ChildCount int
}

// checkMoleculeIntegrity verifies molecule structure integrity
func checkMoleculeIntegrity(db *sql.DB) DoctorCheck {
	check := DoctorCheck{
		Name:     "Molecule Integrity",
		Category: CategoryMetadata,
	}

	// Find molecules (issue_type='molecule' or has beads:template label) with broken structures
	// A molecule should have parent-child relationships forming a valid DAG

	// First, find molecules
	query := `
		SELECT DISTINCT i.id, i.title
		FROM issues i
		LEFT JOIN labels l ON l.issue_id = i.id
		WHERE (i.issue_type = 'molecule' OR l.label = 'beads:template')
		  AND i.status != 'tombstone'
		LIMIT 100`

	rows, err := db.Query(query)
	if err != nil {
		check.Status = StatusWarning
		check.Message = "Unable to find molecules"
		check.Detail = err.Error()
		return check
	}
	defer rows.Close()

	var molecules []moleculeInfo
	for rows.Next() {
		var mol moleculeInfo
		if err := rows.Scan(&mol.ID, &mol.Title); err == nil {
			molecules = append(molecules, mol)
		}
	}

	if len(molecules) == 0 {
		check.Status = StatusOK
		check.Message = "No molecules to validate"
		return check
	}

	// For each molecule, check if all children exist
	var brokenMolecules []string
	for _, mol := range molecules {
		// Count children that don't exist
		var orphanCount int
		err := db.QueryRow(`
			SELECT COUNT(*)
			FROM dependencies d
			WHERE d.depends_on_id = ?
			  AND d.type = 'parent-child'
			  AND NOT EXISTS (SELECT 1 FROM issues WHERE id = d.issue_id AND status != 'tombstone')
		`, mol.ID).Scan(&orphanCount)
		if err == nil && orphanCount > 0 {
			brokenMolecules = append(brokenMolecules, fmt.Sprintf("%s (%d missing children)", mol.ID, orphanCount))
		}
	}

	if len(brokenMolecules) > 0 {
		check.Status = StatusError
		check.Message = fmt.Sprintf("Found %d molecules with missing children", len(brokenMolecules))
		check.Detail = fmt.Sprintf("Examples: %s", strings.Join(brokenMolecules[:min(3, len(brokenMolecules))], ", "))
		check.Fix = "Run 'bd repair-deps' to clean up orphaned relationships"
		return check
	}

	check.Status = StatusOK
	check.Message = fmt.Sprintf("%d molecules validated", len(molecules))
	return check
}

// min returns the smaller of two integers
func min(a, b int) int {
	if a < b {
		return a
	}
	return b
}

// PrintDeepValidationResult prints the deep validation results
func PrintDeepValidationResult(result DeepValidationResult) {
	fmt.Printf("\nDeep Validation Results\n")
	fmt.Printf("========================\n")
	fmt.Printf("Scanned: %d issues, %d dependencies\n\n", result.TotalIssues, result.TotalDependencies)

	for _, check := range result.AllChecks {
		var icon string
		switch check.Status {
		case StatusOK:
			icon = "\u2713" // checkmark
		case StatusWarning:
			icon = "!"
		case StatusError:
			icon = "\u2717" // X
		}
		fmt.Printf("[%s] %s: %s\n", icon, check.Name, check.Message)
		if check.Detail != "" {
			fmt.Printf("    %s\n", check.Detail)
		}
		if check.Fix != "" && check.Status != StatusOK {
			fmt.Printf("    Fix: %s\n", check.Fix)
		}
	}

	fmt.Println()
	if result.OverallOK {
		fmt.Println("All deep validation checks passed!")
	} else {
		fmt.Println("Some checks failed. See details above.")
	}
}

// DeepValidationResultJSON returns the result as JSON bytes
func DeepValidationResultJSON(result DeepValidationResult) ([]byte, error) {
	return json.MarshalIndent(result, "", "  ")
}
