package projects

import (
	"context"
	"embed"
	"fmt"
	"log/slog"
	"os"
	"strings"
	"time"

	"github.com/firebase/genkit/go/ai"
	"github.com/firebase/genkit/go/genkit"
	"github.com/firebase/genkit/go/plugins/compat_oai/openai"
	"github.com/jackc/pgx/v5/pgtype"
	"github.com/jackc/pgx/v5/pgxpool"
	"github.com/openai/openai-go/option"

	"mimi/internal/persist"
	"mimi/internal/provider/github/db"
)

//go:embed prompts
var prompts embed.FS

// Project represents a GitHub project with classification
type Project struct {
	Org               string
	Number            int
	Title             string
	Description       string
	URL               string
	Closed            bool
	Category          string
	CategoryReasoning string
}

// ProjectWithIssues represents a project with its issues
type ProjectWithIssues struct {
	Project
	Issues []db.Issue
}

// Service handles GitHub project operations
type Service struct {
	ghClient         *db.Client
	pgPool           *pgxpool.Pool
	classifierPrompt ai.Prompt
}

// New creates a new project service with its own genkit instance and embedded prompt
func New(ctx context.Context, pgPool *pgxpool.Pool) *Service {
	openrouterAPIKey := os.Getenv("OPENROUTER_API_KEY")
	if openrouterAPIKey == "" {
		panic("OPENROUTER_API_KEY not set")
	}

	openrouterBaseURL := os.Getenv("OPENROUTER_API_URL")
	if openrouterBaseURL == "" {
		panic("OPENROUTER_API_URL not set")
	}

	oai := &openai.OpenAI{
		APIKey: openrouterAPIKey,
		Opts: []option.RequestOption{
			option.WithBaseURL(openrouterBaseURL),
		},
	}

	g := genkit.Init(ctx,
		genkit.WithPlugins(oai),
		genkit.WithPromptFS(prompts),
		genkit.WithPromptDir("prompts"),
		genkit.WithDefaultModel("openai/gpt-4o-mini"),
	)

	oai.DefineModel("openai/gpt-4o-mini", ai.ModelOptions{
		Label:    "GPT-4o Mini",
		Versions: []string{},
		Supports: &ai.ModelSupports{
			Multiturn:   true,
			Tools:       true,
			ToolChoice:  true,
			SystemRole:  true,
			Media:       false,
			Constrained: ai.ConstrainedSupportNone,
		},
		Stage: ai.ModelStageStable,
	})

	classifierPrompt := genkit.LookupPrompt(g, "project-classifier")
	if classifierPrompt == nil {
		panic("project-classifier prompt not found")
	}

	ghClient := db.New("https://api.github.com/graphql")

	return &Service{
		ghClient:         ghClient,
		pgPool:           pgPool,
		classifierPrompt: classifierPrompt,
	}
}

// FetchActiveProjects returns all open projects with their issues.
// It handles sync, classification, and retries if a project is not found (deleted/renamed).
func (s *Service) FetchActiveProjects(ctx context.Context, org string, since time.Time) ([]ProjectWithIssues, error) {
	if err := s.ensureProjectsExist(ctx, org); err != nil {
		return nil, fmt.Errorf("failed to ensure projects exist: %w", err)
	}

	projects, err := s.getActiveProjects(ctx, org)
	if err != nil {
		return nil, fmt.Errorf("failed to get active projects: %w", err)
	}

	return s.fetchProjectIssues(ctx, org, projects, since, true)
}

// FetchProjectsByCategory returns projects filtered by category with their issues.
func (s *Service) FetchProjectsByCategory(ctx context.Context, org, category string, since time.Time) ([]ProjectWithIssues, error) {
	if err := s.ensureProjectsExist(ctx, org); err != nil {
		return nil, fmt.Errorf("failed to ensure projects exist: %w", err)
	}

	projects, err := s.getProjectsByCategory(ctx, org, category)
	if err != nil {
		return nil, fmt.Errorf("failed to get projects by category: %w", err)
	}

	return s.fetchProjectIssues(ctx, org, projects, since, true)
}

// fetchProjectIssues fetches issues for each project, with retry on failure
func (s *Service) fetchProjectIssues(ctx context.Context, org string, projects []Project, since time.Time, allowRetry bool) ([]ProjectWithIssues, error) {
	result := make([]ProjectWithIssues, 0, len(projects))
	var failedProjects []int

	for _, proj := range projects {
		issues, err := s.ghClient.GetOrgProject(ctx, org, proj.Number, since)
		if err != nil {
			// Check if it's a "not found" error (project deleted/renamed)
			if isProjectNotFoundError(err) {
				slog.Warn("project not found, marking for resync",
					"project", proj.Title,
					"number", proj.Number,
					"error", err)
				failedProjects = append(failedProjects, proj.Number)
				continue
			}
			// For other errors, fail immediately
			return nil, fmt.Errorf("failed to fetch issues for project %d (%s): %w", proj.Number, proj.Title, err)
		}

		slog.Info("fetched project issues",
			"project", proj.Title,
			"category", proj.Category,
			"count", len(issues))

		result = append(result, ProjectWithIssues{
			Project: proj,
			Issues:  issues,
		})
	}

	// If we had failed projects and retry is allowed, resync and retry
	if len(failedProjects) > 0 && allowRetry {
		slog.Info("some projects not found, resyncing from GitHub", "failed", failedProjects)

		// Force resync from GitHub
		if err := s.syncProjects(ctx, org); err != nil {
			return nil, fmt.Errorf("failed to resync projects: %w", err)
		}

		// Get updated project list
		updatedProjects, err := s.getActiveProjects(ctx, org)
		if err != nil {
			return nil, fmt.Errorf("failed to get updated projects: %w", err)
		}

		// Retry fetching without allowing another retry (prevent infinite loop)
		return s.fetchProjectIssues(ctx, org, updatedProjects, since, false)
	}

	return result, nil
}

// isProjectNotFoundError checks if the error indicates a project was not found
func isProjectNotFoundError(err error) bool {
	if err == nil {
		return false
	}
	errStr := err.Error()
	return strings.Contains(errStr, "Could not resolve to a ProjectV2") ||
		strings.Contains(errStr, "NOT_FOUND")
}

// ensureProjectsExist checks if projects exist in DB, syncs if needed
func (s *Service) ensureProjectsExist(ctx context.Context, org string) error {
	needsSync, err := s.needsSyncCheck(ctx, org)
	if err != nil {
		slog.Warn("failed to check if sync needed, forcing sync", "error", err)
		needsSync = true
	}

	if needsSync {
		slog.Info("project changes detected, syncing from GitHub")
		if err := s.syncProjects(ctx, org); err != nil {
			return fmt.Errorf("failed to sync projects: %w", err)
		}
	}

	return nil
}

// needsSyncCheck determines if we should sync projects from GitHub
func (s *Service) needsSyncCheck(ctx context.Context, org string) (bool, error) {
	q := persist.New(s.pgPool)
	dbProjects, err := q.GetAllGitHubProjects(ctx, org)
	if err != nil {
		return true, fmt.Errorf("failed to get projects from db: %w", err)
	}

	// If no projects in DB, definitely need sync
	if len(dbProjects) == 0 {
		slog.Info("no projects in database, sync needed")
		return true, nil
	}

	// Fetch current projects from GitHub
	githubProjects, err := s.ghClient.ListProjects(ctx, org)
	if err != nil {
		slog.Warn("failed to check GitHub projects, assuming no sync needed", "error", err)
		return false, nil
	}

	// Build maps for comparison
	dbMap := make(map[int]string)
	for _, p := range dbProjects {
		dbMap[int(p.Number)] = p.Title
	}

	ghMap := make(map[int]string)
	for _, p := range githubProjects {
		ghMap[p.Id] = p.Title
	}

	// Check if any project was added, removed, or renamed
	if len(dbMap) != len(ghMap) {
		slog.Info("project count changed, sync needed", "db", len(dbMap), "github", len(ghMap))
		return true, nil
	}

	for num, title := range ghMap {
		dbTitle, exists := dbMap[num]
		if !exists {
			slog.Info("new project found, sync needed", "number", num, "title", title)
			return true, nil
		}
		if dbTitle != title {
			slog.Info("project renamed, sync needed", "number", num, "old", dbTitle, "new", title)
			return true, nil
		}
	}

	slog.Info("no project changes detected")
	return false, nil
}

// syncProjects fetches projects from GitHub API and updates database
func (s *Service) syncProjects(ctx context.Context, org string) error {
	slog.Info("syncing GitHub projects", "org", org)

	// Fetch from GitHub API
	githubProjects, err := s.ghClient.ListProjects(ctx, org)
	if err != nil {
		return fmt.Errorf("failed to list projects from GitHub: %w", err)
	}

	slog.Info("fetched projects from GitHub", "count", len(githubProjects))

	// Convert to our Project type
	projects := make([]Project, 0, len(githubProjects))
	for _, gp := range githubProjects {
		projects = append(projects, Project{
			Org:         org,
			Number:      gp.Id,
			Title:       gp.Title,
			Description: gp.ShortDescription,
			URL:         gp.URL,
			Closed:      gp.Closed,
		})
	}

	// Get existing projects to preserve their classifications
	q := persist.New(s.pgPool)
	existingProjects, err := q.GetAllGitHubProjects(ctx, org)
	if err != nil {
		slog.Warn("failed to get existing projects", "error", err)
	}

	// Build map of existing classifications
	existingClassifications := make(map[int]struct {
		Category  string
		Reasoning string
	})
	for _, ep := range existingProjects {
		if ep.Category.Valid {
			existingClassifications[int(ep.Number)] = struct {
				Category  string
				Reasoning string
			}{
				Category:  ep.Category.String,
				Reasoning: ep.CategoryReasoning.String,
			}
		}
	}

	// Preserve existing classifications
	for i := range projects {
		if existing, ok := existingClassifications[projects[i].Number]; ok {
			projects[i].Category = existing.Category
			projects[i].CategoryReasoning = existing.Reasoning
		}
	}

	// Classify projects that don't have classification yet
	unclassified := make([]Project, 0)
	for _, proj := range projects {
		if proj.Category == "" && !proj.Closed {
			unclassified = append(unclassified, proj)
		}
	}

	if len(unclassified) > 0 {
		if err := s.classifyProjects(ctx, unclassified); err != nil {
			return fmt.Errorf("failed to classify projects: %w", err)
		}

		// Apply classifications back to main list
		classificationMap := make(map[int]Project)
		for _, p := range unclassified {
			classificationMap[p.Number] = p
		}
		for i := range projects {
			if classified, ok := classificationMap[projects[i].Number]; ok {
				projects[i].Category = classified.Category
				projects[i].CategoryReasoning = classified.CategoryReasoning
			}
		}
	}

	// Store in database
	for _, proj := range projects {
		var category, categoryReasoning pgtype.Text
		if proj.Category != "" {
			category = pgtype.Text{String: proj.Category, Valid: true}
			categoryReasoning = pgtype.Text{String: proj.CategoryReasoning, Valid: true}
		}

		err := q.UpsertGitHubProject(ctx, persist.UpsertGitHubProjectParams{
			Org:               proj.Org,
			Number:            int32(proj.Number),
			Title:             proj.Title,
			Description:       pgtype.Text{String: proj.Description, Valid: proj.Description != ""},
			Url:               proj.URL,
			Closed:            proj.Closed,
			Category:          category,
			CategoryReasoning: categoryReasoning,
		})
		if err != nil {
			slog.Error("failed to upsert project", "project", proj.Number, "error", err)
		}
	}

	slog.Info("synced projects to database", "count", len(projects))
	return nil
}

// getActiveProjects returns all open projects from database
func (s *Service) getActiveProjects(ctx context.Context, org string) ([]Project, error) {
	q := persist.New(s.pgPool)
	rows, err := q.GetActiveGitHubProjects(ctx, org)
	if err != nil {
		return nil, fmt.Errorf("failed to get active projects: %w", err)
	}

	projects := make([]Project, 0, len(rows))
	for _, row := range rows {
		projects = append(projects, Project{
			Org:               row.Org,
			Number:            int(row.Number),
			Title:             row.Title,
			Description:       row.Description.String,
			URL:               row.Url,
			Closed:            row.Closed,
			Category:          row.Category.String,
			CategoryReasoning: row.CategoryReasoning.String,
		})
	}

	return projects, nil
}

// getProjectsByCategory returns projects filtered by category from database
func (s *Service) getProjectsByCategory(ctx context.Context, org, category string) ([]Project, error) {
	q := persist.New(s.pgPool)
	rows, err := q.GetGitHubProjectsByCategory(ctx, persist.GetGitHubProjectsByCategoryParams{
		Org:      org,
		Category: pgtype.Text{String: category, Valid: true},
	})
	if err != nil {
		return nil, fmt.Errorf("failed to get projects by category: %w", err)
	}

	projects := make([]Project, 0, len(rows))
	for _, row := range rows {
		projects = append(projects, Project{
			Org:               row.Org,
			Number:            int(row.Number),
			Title:             row.Title,
			Description:       row.Description.String,
			URL:               row.Url,
			Closed:            row.Closed,
			Category:          row.Category.String,
			CategoryReasoning: row.CategoryReasoning.String,
		})
	}

	return projects, nil
}

Graph