package lessons

import (
	"fmt"
	"os"
	"path/filepath"
	"regexp"
	"strings"
	"sync"
	"time"

	"gopkg.in/yaml.v3"
)

type LessonMode string

const (
	ModeInteractive LessonMode = "interactive"
	ModeAutonomous  LessonMode = "autonomous"
	ModeBoth        LessonMode = "both"
)

type Lesson struct {
	Name        string       `yaml:"name" json:"name"`
	Title       string       `yaml:"title" json:"title"`
	Description string       `yaml:"description" json:"description"`
	Category    string       `yaml:"category" json:"category"`
	Triggers    Triggers     `yaml:"triggers" json:"triggers"`
	Content     string       `yaml:"content" json:"content"`
	Mode        LessonMode   `yaml:"mode" json:"mode"`
	Priority    int          `yaml:"priority" json:"priority"`
	Enabled     bool         `yaml:"enabled" json:"enabled"`
	Path        string       `yaml:"-" json:"path,omitempty"`
}

type Triggers struct {
	Keywords []string `yaml:"keywords" json:"keywords"`
	Tools    []string `yaml:"tools" json:"tools"`
	Patterns []string `yaml:"patterns" json:"patterns"`
}

type MatchContext struct {
	Message   string   `json:"message"`
	ToolsUsed []string `json:"tools_used,omitempty"`
	Mode      string   `json:"mode,omitempty"`
}

type MatchResult struct {
	Lesson *Lesson `json:"lesson"`
	Score  float64 `json:"score"`
}

type LessonFrontmatter struct {
	Name        string     `yaml:"name"`
	Title       string     `yaml:"title"`
	Description string     `yaml:"description"`
	Category    string     `yaml:"category"`
	Mode        LessonMode `yaml:"mode"`
	Priority    int        `yaml:"priority"`
	Enabled     *bool      `yaml:"enabled"`
	Triggers    Triggers   `yaml:"triggers"`
}

type LessonIndex struct {
	mu      sync.RWMutex
	lessons []*Lesson
	paths   []string
	cache   map[string]time.Time
}

var (
	globalIndex     *LessonIndex
	globalIndexOnce sync.Once
)

func GetIndex() *LessonIndex {
	globalIndexOnce.Do(func() {
		globalIndex = &LessonIndex{
			lessons: make([]*Lesson, 0),
			cache:   make(map[string]time.Time),
		}
		globalIndex.paths = DefaultLessonDirs()
		globalIndex.Reload()
	})
	return globalIndex
}

func DefaultLessonDirs() []string {
	var dirs []string

	home, _ := os.UserHomeDir()
	if home != "" {
		dirs = append(dirs,
			filepath.Join(home, ".muyue", "lessons"),
		)
	}

	configDir, err := os.UserConfigDir()
	if err == nil {
		dirs = append(dirs, filepath.Join(configDir, "muyue", "lessons"))
	}

	if extra := os.Getenv("MUYUE_LESSONS_EXTRA_DIRS"); extra != "" {
		for _, d := range strings.Split(extra, ":") {
			d = strings.TrimSpace(d)
			if d != "" {
				dirs = append(dirs, d)
			}
		}
	}

	return dirs
}

func (idx *LessonIndex) Reload() {
	idx.mu.Lock()
	defer idx.mu.Unlock()

	var all []*Lesson
	seen := make(map[string]bool)

	for _, dir := range idx.paths {
		files, err := filepath.Glob(filepath.Join(dir, "*.md"))
		if err != nil {
			continue
		}

		for _, f := range files {
			realPath, _ := filepath.EvalSymlinks(f)
			if realPath == "" {
				realPath = f
			}
			if seen[realPath] {
				continue
			}
			seen[realPath] = true

			lesson, err := ParseLessonFile(f)
			if err != nil {
				continue
			}
			lesson.Path = f
			if lesson.Category == "" {
				lesson.Category = filepath.Base(filepath.Dir(f))
			}
			all = append(all, lesson)
		}

		subDirs, _ := filepath.Glob(filepath.Join(dir, "*"))
		for _, subDir := range subDirs {
			info, err := os.Stat(subDir)
			if err != nil || !info.IsDir() {
				continue
			}
			category := filepath.Base(subDir)
			subFiles, _ := filepath.Glob(filepath.Join(subDir, "*.md"))
			for _, f := range subFiles {
				realPath, _ := filepath.EvalSymlinks(f)
				if realPath == "" {
					realPath = f
				}
				if seen[realPath] {
					continue
				}
				seen[realPath] = true

				lesson, err := ParseLessonFile(f)
				if err != nil {
					continue
				}
				lesson.Path = f
				if lesson.Category == "" {
					lesson.Category = category
				}
				all = append(all, lesson)
			}
		}
	}

	idx.lessons = all
}

func (idx *LessonIndex) All() []*Lesson {
	idx.mu.RLock()
	defer idx.mu.RUnlock()

	result := make([]*Lesson, 0, len(idx.lessons))
	for _, l := range idx.lessons {
		if l.Enabled {
			result = append(result, l)
		}
	}
	return result
}

func (idx *LessonIndex) Get(name string) *Lesson {
	idx.mu.RLock()
	defer idx.mu.RUnlock()

	for _, l := range idx.lessons {
		if l.Name == name {
			return l
		}
	}
	return nil
}

func (idx *LessonIndex) Count() int {
	idx.mu.RLock()
	defer idx.mu.RUnlock()
	return len(idx.lessons)
}

func ParseLessonFile(path string) (*Lesson, error) {
	data, err := os.ReadFile(path)
	if err != nil {
		return nil, fmt.Errorf("read lesson: %w", err)
	}

	content := string(data)

	var frontmatter LessonFrontmatter
	var body string

	if strings.HasPrefix(content, "---") {
		end := strings.Index(content[3:], "---")
		if end != -1 {
			fm := content[3 : end+3]
			body = strings.TrimSpace(content[end+6:])

			if err := yaml.Unmarshal([]byte(fm), &frontmatter); err != nil {
				body = content
			}
		} else {
			body = content
		}
	} else {
		body = content
	}

	enabled := true
	if frontmatter.Enabled != nil {
		enabled = *frontmatter.Enabled
	}

	if frontmatter.Mode == "" {
		frontmatter.Mode = ModeBoth
	}

	name := frontmatter.Name
	if name == "" {
		name = strings.TrimSuffix(filepath.Base(path), ".md")
		name = strings.ReplaceAll(name, "-", "_")
	}

	return &Lesson{
		Name:        name,
		Title:       frontmatter.Title,
		Description: frontmatter.Description,
		Category:    frontmatter.Category,
		Triggers:    frontmatter.Triggers,
		Content:     body,
		Mode:        frontmatter.Mode,
		Priority:    frontmatter.Priority,
		Enabled:     enabled,
	}, nil
}

func Match(lessons []*Lesson, ctx MatchContext) []*MatchResult {
	var results []*MatchResult
	msgLower := strings.ToLower(ctx.Message)

	for _, l := range lessons {
		if !l.Enabled {
			continue
		}

		score := 0.0

		for _, kw := range l.Triggers.Keywords {
			if containsKeyword(msgLower, strings.ToLower(kw)) {
				score += 1.0
			}
		}

		for _, pattern := range l.Triggers.Patterns {
			re, err := regexp.Compile("(?i)" + pattern)
			if err == nil && re.MatchString(ctx.Message) {
				score += 1.5
			}
		}

		if len(ctx.ToolsUsed) > 0 && len(l.Triggers.Tools) > 0 {
			for _, usedTool := range ctx.ToolsUsed {
				for _, triggerTool := range l.Triggers.Tools {
					if usedTool == triggerTool {
						score += 2.0
						break
					}
				}
			}
		}

		if l.Name != "" {
			nameLower := strings.ToLower(l.Name)
			if strings.Contains(msgLower, nameLower) {
				score += 1.5
			}
		}

		if score > 0 {
			results = append(results, &MatchResult{
				Lesson: l,
				Score:  score,
			})
		}
	}

	sortResults(results)
	return results
}

func AutoInclude(systemPrompt string, lessons []*Lesson, ctx MatchContext, maxLessons int) string {
	if maxLessons <= 0 {
		maxLessons = 5
	}

	results := Match(lessons, ctx)
	if len(results) == 0 {
		return systemPrompt
	}

	if len(results) > maxLessons {
		results = results[:maxLessons]
	}

	var lessonBlock strings.Builder
	lessonBlock.WriteString("\n\n--- Active Lessons ---\n\n")

	for _, r := range results {
		lessonBlock.WriteString(fmt.Sprintf("## %s", r.Lesson.Name))
		if r.Lesson.Title != "" {
			lessonBlock.WriteString(fmt.Sprintf(" (%s)", r.Lesson.Title))
		}
		lessonBlock.WriteString("\n")
		lessonBlock.WriteString(r.Lesson.Content)
		lessonBlock.WriteString("\n\n")
	}

	return systemPrompt + lessonBlock.String()
}

func EnsureBuiltinLessons() error {
	home, _ := os.UserHomeDir()
	if home == "" {
		return nil
	}

	lessonsDir := filepath.Join(home, ".muyue", "lessons")
	if err := os.MkdirAll(lessonsDir, 0755); err != nil {
		return err
	}

	for _, lesson := range BuiltinLessons() {
		path := filepath.Join(lessonsDir, lesson.Name+".md")
		if _, err := os.Stat(path); err == nil {
			continue
		}
		if err := WriteLesson(path, lesson); err != nil {
			_ = err
		}
	}

	return nil
}

func WriteLesson(path string, lesson *Lesson) error {
	var sb strings.Builder

	sb.WriteString("---\n")
	data, err := yaml.Marshal(&LessonFrontmatter{
		Name:        lesson.Name,
		Title:       lesson.Title,
		Description: lesson.Description,
		Category:    lesson.Category,
		Mode:        lesson.Mode,
		Priority:    lesson.Priority,
		Enabled:     &lesson.Enabled,
		Triggers:    lesson.Triggers,
	})
	if err != nil {
		return err
	}
	sb.WriteString(string(data))
	sb.WriteString("---\n\n")
	sb.WriteString(lesson.Content)

	return os.WriteFile(path, []byte(sb.String()), 0644)
}

func BuiltinLessons() []*Lesson {
	return []*Lesson{
		{
			Name:        "code_style",
			Title:       "Code Style Guidelines",
			Description: "Enforce consistent code style and formatting",
			Category:    "development",
			Triggers: Triggers{
				Keywords: []string{"code style", "formatting", "lint", "format", "indentation", "naming convention"},
				Tools:    []string{"terminal"},
			},
			Content: `- Follow the existing code style in each file
- Use consistent indentation (match surrounding code)
- Prefer descriptive variable names over abbreviations
- Keep functions focused and small
- Add error handling for all external calls`,
			Mode:     ModeBoth,
			Priority: 5,
			Enabled:  true,
		},
		{
			Name:        "git_workflow",
			Title:       "Git Workflow Best Practices",
			Description: "Guidelines for git operations and commit practices",
			Category:    "development",
			Triggers: Triggers{
				Keywords: []string{"git", "commit", "branch", "merge", "pull request", "rebase"},
				Tools:    []string{"terminal"},
			},
			Content: `- Write clear, descriptive commit messages
- Use conventional commits format when applicable
- Keep commits atomic and focused
- Don't commit sensitive data or secrets
- Test before committing`,
			Mode:     ModeBoth,
			Priority: 5,
			Enabled:  true,
		},
		{
			Name:        "error_handling",
			Title:       "Error Handling Patterns",
			Description: "Robust error handling guidelines",
			Category:    "development",
			Triggers: Triggers{
				Keywords: []string{"error", "panic", "exception", "crash", "fail", "nil pointer"},
				Tools:    []string{"terminal", "read_file"},
				Patterns: []string{`err\s*!=\s*nil`, `panic\(`, `log\.Fatal`},
			},
			Content: `- Always check errors from external calls
- Provide context when wrapping errors
- Use sentinel errors for expected conditions
- Log errors with enough context for debugging
- Don't silently ignore errors`,
			Mode:     ModeBoth,
			Priority: 6,
			Enabled:  true,
		},
		{
			Name:        "testing",
			Title:       "Testing Best Practices",
			Description: "Guidelines for writing effective tests",
			Category:    "development",
			Triggers: Triggers{
				Keywords: []string{"test", "testing", "unit test", "integration test", "coverage"},
				Tools:    []string{"terminal"},
			},
			Content: `- Write tests for critical paths first
- Use table-driven tests for multiple cases
- Keep tests independent and deterministic
- Test error paths, not just happy paths
- Aim for meaningful coverage, not just percentage`,
			Mode:     ModeBoth,
			Priority: 5,
			Enabled:  true,
		},
		{
			Name:        "security",
			Title:       "Security Guidelines",
			Description: "Security best practices for development",
			Category:    "development",
			Triggers: Triggers{
				Keywords: []string{"security", "vulnerability", "inject", "sanitize", "auth", "secret", "password", "token"},
				Tools:    []string{"terminal", "read_file", "web_fetch"},
				Patterns: []string{`SELECT\s.*\+`, `exec\.Command.*\+`, `os\.Getenv.*KEY`},
			},
			Content: `- Never log or expose secrets, API keys, or tokens
- Validate and sanitize all user input
- Use parameterized queries for database operations
- Keep dependencies updated
- Don't hardcode credentials`,
			Mode:     ModeBoth,
			Priority: 8,
			Enabled:  true,
		},
	}
}

func containsKeyword(text, keyword string) bool {
	if keyword == "*" {
		return true
	}
	return strings.Contains(text, keyword)
}

func sortResults(results []*MatchResult) {
	for i := 0; i < len(results)-1; i++ {
		for j := i + 1; j < len(results); j++ {
			if results[j].Score > results[i].Score {
				results[i], results[j] = results[j], results[i]
			}
		}
	}
}
