package generator

import (
	"errors"
	"termdoku/internal/solver"
	"time"
)

// Difficulty represents puzzle difficulty tiers.
type Difficulty int

const (
	Easy Difficulty = iota
	Normal
	Hard
	Expert
	Lunatic
)

// String returns the string representation of the difficulty.
func (d Difficulty) String() string {
	switch d {
	case Easy:
		return "Easy"
	case Normal:
		return "Normal"
	case Hard:
		return "Hard"
	case Expert:
		return "Expert"
	case Lunatic:
		return "Lunatic"
	default:
		return "Unknown"
	}
}

// DailySeed returns a stable seed based on UTC date (YYYY-MM-DD).
func DailySeed(t time.Time) string {
	utc := t.UTC()
	return utc.Format("2006-01-02")
}

// Params controls generation knobs derived from difficulty.
type Params struct {
	// number of blanks/removed cells; higher -> harder
	RemovedCells int
	// backtracking timeout to avoid worst-cases
	Timeout time.Duration
	// symmetry pattern to use (optional)
	Symmetry SymmetryType
	// whether to enforce symmetry
	UseSymmetry bool
}

// paramsFor maps Difficulty to generation parameters.
func paramsFor(d Difficulty) Params {
	switch d {
	case Easy:
		return Params{
			RemovedCells: 38,
			Timeout:      150 * time.Millisecond,
			UseSymmetry:  false,
		}
	case Normal:
		return Params{
			RemovedCells: 46,
			Timeout:      150 * time.Millisecond,
			UseSymmetry:  false,
		}
	case Hard:
		return Params{
			RemovedCells: 52,
			Timeout:      200 * time.Millisecond,
			UseSymmetry:  false,
		}
	case Expert:
		return Params{
			RemovedCells: 56,
			Timeout:      250 * time.Millisecond,
			UseSymmetry:  false,
		}
	case Lunatic:
		return Params{
			RemovedCells: 60,
			Timeout:      300 * time.Millisecond,
			UseSymmetry:  false,
		}
	default:
		return Params{
			RemovedCells: 46,
			Timeout:      150 * time.Millisecond,
			UseSymmetry:  false,
		}
	}
}

// Grid is a 9x9 Sudoku grid. 0 represents empty.
type Grid [9][9]uint8

// ErrTimeout is returned when generation exceeds the configured timeout.
var ErrTimeout = errors.New("generation timed out")

// Generate creates a Sudoku puzzle with the given difficulty and seed.
// - If seed is empty, uses current time for randomness.
// - For Daily mode, pass seed from DailySeed(date).
// Returns a puzzle grid with 0 as blanks, aimed at single-solution.
func Generate(d Difficulty, seed string) (Grid, error) {
	p := paramsFor(d)
	return generateWithParams(p, seed)
}

// GenerateDaily creates a daily puzzle based on UTC date.
func GenerateDaily(date time.Time) (Grid, error) {
	return Generate(Normal, DailySeed(date))
}

// GenerateWithSymmetry creates a puzzle with a specific symmetry pattern.
func GenerateWithSymmetry(d Difficulty, seed string, symmetry SymmetryType) (Grid, error) {
	p := paramsFor(d)
	p.UseSymmetry = true
	p.Symmetry = symmetry
	return generateWithParams(p, seed)
}

// GenerateCustom creates a puzzle with custom parameters.
func GenerateCustom(removedCells int, seed string, useSymmetry bool, symmetry SymmetryType) (Grid, error) {
	p := Params{
		RemovedCells: removedCells,
		Timeout:      300 * time.Millisecond,
		UseSymmetry:  useSymmetry,
		Symmetry:     symmetry,
	}
	return generateWithParams(p, seed)
}

// PuzzleWithSolution represents a puzzle along with its solution.
type PuzzleWithSolution struct {
	Puzzle   Grid
	Solution Grid
	Analysis PuzzleAnalysis
}

// GenerateWithAnalysis creates a puzzle and returns it with its solution and analysis.
func GenerateWithAnalysis(d Difficulty, seed string) (PuzzleWithSolution, error) {
	puzzle, err := Generate(d, seed)
	if err != nil {
		return PuzzleWithSolution{}, err
	}

	// Solve to get the solution
	solution := puzzle.Clone()
	solverGrid := convertToSolverGrid(solution)
	if !solver.Solve(&solverGrid, 500*time.Millisecond) {
		return PuzzleWithSolution{}, errors.New("failed to solve generated puzzle")
	}

	// Convert back to Grid
	for r := 0; r < 9; r++ {
		for c := 0; c < 9; c++ {
			solution[r][c] = solverGrid[r][c]
		}
	}

	// Analyze the puzzle
	analysis := puzzle.Analyze()

	return PuzzleWithSolution{
		Puzzle:   puzzle,
		Solution: solution,
		Analysis: analysis,
	}, nil
}

// RatePuzzle provides a difficulty rating from 0-100 based on puzzle characteristics.
func RatePuzzle(g Grid) int {
	analysis := g.Analyze()

	// Base score from empty cells (0-40 points)
	emptyScore := (analysis.EmptyCells * 40) / 81

	// Candidate complexity (0-30 points)
	candidateScore := 0
	if analysis.AvgCandidates > 0 {
		// Lower average candidates = harder
		candidateScore = int((9.0 - analysis.AvgCandidates) * 3.3)
		if candidateScore < 0 {
			candidateScore = 0
		}
		if candidateScore > 30 {
			candidateScore = 30
		}
	}

	// Technique complexity (0-30 points)
	techniqueScore := 0
	for _, tech := range analysis.SolvingTechniques {
		if tech == "Advanced Techniques Required" {
			techniqueScore = 30
			break
		} else if tech == "Hidden Singles" {
			techniqueScore = 15
		} else if tech == "Naked Singles" {
			techniqueScore = 5
		}
	}

	totalScore := emptyScore + candidateScore + techniqueScore
	if totalScore > 100 {
		totalScore = 100
	}

	return totalScore
}

// DifficultyFromRating converts a rating (0-100) to a Difficulty level.
func DifficultyFromRating(rating int) Difficulty {
	if rating >= 85 {
		return Lunatic
	} else if rating >= 70 {
		return Expert
	} else if rating >= 50 {
		return Hard
	} else if rating >= 30 {
		return Normal
	}
	return Easy
}

// generateWithParams contains the core generation pipeline.
func generateWithParams(p Params, seed string) (Grid, error) {
	// 1) Create a full valid solution via randomized backtracking
	full, err := randomizedFullSolution(seed, p.Timeout)
	if err != nil {
		return Grid{}, err
	}
	// 2) Remove cells according to difficulty while keeping uniqueness if possible
	var puzzle Grid
	if p.UseSymmetry {
		puzzle, err = carveCellsSymmetric(full, p.RemovedCells, seed, p.Timeout, p.Symmetry)
	} else {
		puzzle, err = carveCellsUnique(full, p.RemovedCells, seed, p.Timeout)
	}
	if err != nil {
		return Grid{}, err
	}
	return puzzle, nil
}
