package main

import (
	"bytes"
	"fmt"
	"html/template"
	"log"
	"net/url"
	"os"
	"path"
	"path/filepath"
	"sort"
	"strconv"
	"strings"
	"time"
)

func init() {
	pageTypeHandlers = append(pageTypeHandlers, &diaryHandler{})
}

type diaryHandler struct{}

func (d *diaryHandler) handle(root, fsPath, urlPath string) *specialPage {
	depth, diaryRootFS, diaryRootURL, ok := findDiaryContext(root, fsPath, urlPath)
	if !ok {
		return nil
	}
	widget := computeCalendarWidget(diaryRootFS, diaryRootURL, fsPath, depth)
	if depth == 0 {
		return &specialPage{Widget: widget}
	}
	var content template.HTML
	switch depth {
	case 1:
		content = renderDiaryYear(fsPath, urlPath)
	case 2:
		content = renderDiaryMonth(fsPath, urlPath)
	case 3:
		content = renderDiaryDay(fsPath, urlPath)
	}
	return &specialPage{Content: content, SuppressListing: true, Widget: widget}
}

// findDiaryContext walks up from fsPath toward root looking for a
// .page-settings file with type=diary. Returns the depth of fsPath
// relative to the diary root, the diary root fs path, its URL, and
// whether a diary root was found. depth=0 means fsPath itself is the root.
func findDiaryContext(root, fsPath, urlPath string) (depth int, diaryRootFS, diaryRootURL string, ok bool) {
	currentFS := fsPath
	currentURL := urlPath
	for d := 0; ; d++ {
		s := readPageSettings(currentFS)
		if s != nil && s.Type == "diary" {
			return d, currentFS, currentURL, true
		}
		if currentFS == root {
			break
		}
		parent := filepath.Dir(currentFS)
		if parent == currentFS {
			break
		}
		currentFS = parent
		currentURL = parentURL(currentURL)
	}
	return 0, "", "", false
}

type calDay struct {
	Num       int
	URL       string
	HasEntry  bool
	IsToday   bool
	IsCurrent bool
}

type calYear struct {
	Num       int
	URL       string
	IsCurrent bool
}

type calMonth struct {
	Num       int
	Name      string
	URL       string
	IsCurrent bool
}

type calendarData struct {
	DisplayYear  int
	DisplayMonth int
	MonthName    string
	DiaryURL     string
	YearURL      string
	MonthURL     string
	PrevMonURL   string
	NextMonURL   string
	Weeks        [][]calDay
	Years        []calYear
	AllMonths    []calMonth
}

var diaryCalTmpl = template.Must(template.ParseFS(assets, "assets/diary/diary-calendar.html"))

func computeCalendarWidget(diaryRootFS, diaryRootURL, fsPath string, depth int) template.HTML {
	today := time.Now()
	var displayYear, displayMonth, currentDay int

	switch depth {
	case 0:
		displayYear = today.Year()
		displayMonth = int(today.Month())
	case 1:
		y, err := strconv.Atoi(filepath.Base(fsPath))
		if err != nil {
			return ""
		}
		displayYear = y
		if y == today.Year() {
			displayMonth = int(today.Month())
		} else {
			displayMonth = 1
		}
	case 2:
		m, err := strconv.Atoi(filepath.Base(fsPath))
		if err != nil || m < 1 || m > 12 {
			return ""
		}
		y, err := strconv.Atoi(filepath.Base(filepath.Dir(fsPath)))
		if err != nil {
			return ""
		}
		displayYear = y
		displayMonth = m
	case 3:
		d, err := strconv.Atoi(filepath.Base(fsPath))
		if err != nil || d < 1 || d > 31 {
			return ""
		}
		monthFS := filepath.Dir(fsPath)
		m, err := strconv.Atoi(filepath.Base(monthFS))
		if err != nil || m < 1 || m > 12 {
			return ""
		}
		y, err := strconv.Atoi(filepath.Base(filepath.Dir(monthFS)))
		if err != nil {
			return ""
		}
		displayYear = y
		displayMonth = m
		currentDay = d
	default:
		return ""
	}

	// Which days in the display month have diary subfolders?
	monthFSPath := filepath.Join(diaryRootFS,
		fmt.Sprintf("%d", displayYear),
		fmt.Sprintf("%02d", displayMonth))
	dayEntries, _ := os.ReadDir(monthFSPath)
	hasDayEntry := map[int]bool{}
	for _, e := range dayEntries {
		if !e.IsDir() {
			continue
		}
		d, err := strconv.Atoi(e.Name())
		if err != nil || d < 1 || d > 31 {
			continue
		}
		hasDayEntry[d] = true
	}

	// Build calendar grid with Monday as first column.
	firstDay := time.Date(displayYear, time.Month(displayMonth), 1, 0, 0, 0, 0, time.UTC)
	startOffset := int(firstDay.Weekday()+6) % 7
	daysInMonth := time.Date(displayYear, time.Month(displayMonth)+1, 0, 0, 0, 0, 0, time.UTC).Day()

	monthURLBase := path.Join(diaryRootURL,
		fmt.Sprintf("%d", displayYear),
		fmt.Sprintf("%02d", displayMonth)) + "/"

	var weeks [][]calDay
	week := make([]calDay, 7)
	col := startOffset
	for d := 1; d <= daysInMonth; d++ {
		dayURL := path.Join(monthURLBase, fmt.Sprintf("%02d", d)) + "/"
		cell := calDay{Num: d, HasEntry: hasDayEntry[d]}
		if cell.HasEntry {
			cell.URL = dayURL
		} else {
			cell.URL = dayURL + "?edit"
		}
		cell.IsCurrent = d == currentDay
		cell.IsToday = d == today.Day() &&
			time.Month(displayMonth) == today.Month() &&
			displayYear == today.Year()
		week[col] = cell
		col++
		if col == 7 {
			weeks = append(weeks, week)
			week = make([]calDay, 7)
			col = 0
		}
	}
	if col > 0 {
		weeks = append(weeks, week)
	}

	prev := time.Date(displayYear, time.Month(displayMonth)-1, 1, 0, 0, 0, 0, time.UTC)
	next := time.Date(displayYear, time.Month(displayMonth)+1, 1, 0, 0, 0, 0, time.UTC)
	prevMonURL := path.Join(diaryRootURL,
		fmt.Sprintf("%d", prev.Year()),
		fmt.Sprintf("%02d", int(prev.Month()))) + "/"
	nextMonURL := path.Join(diaryRootURL,
		fmt.Sprintf("%d", next.Year()),
		fmt.Sprintf("%02d", int(next.Month()))) + "/"
	yearURL := path.Join(diaryRootURL, fmt.Sprintf("%d", displayYear)) + "/"

	// Collect all year subdirectories in diary root (descending).
	yearEntries, _ := os.ReadDir(diaryRootFS)
	var years []calYear
	yearSet := map[int]bool{}
	for _, e := range yearEntries {
		if !e.IsDir() {
			continue
		}
		y, err := strconv.Atoi(e.Name())
		if err != nil {
			continue
		}
		yearSet[y] = true
		years = append(years, calYear{
			Num:       y,
			URL:       path.Join(diaryRootURL, e.Name()) + "/",
			IsCurrent: y == displayYear,
		})
	}
	if !yearSet[displayYear] {
		years = append(years, calYear{
			Num:       displayYear,
			URL:       yearURL,
			IsCurrent: true,
		})
	}
	sort.Slice(years, func(i, j int) bool { return years[i].Num > years[j].Num })

	// All 12 months, ascending, linked to current display year.
	allMonths := make([]calMonth, 12)
	for m := 1; m <= 12; m++ {
		allMonths[m-1] = calMonth{
			Num:  m,
			Name: germanMonths[time.Month(m)],
			URL: path.Join(diaryRootURL,
				fmt.Sprintf("%d", displayYear),
				fmt.Sprintf("%02d", m)) + "/",
			IsCurrent: m == displayMonth,
		}
	}

	data := calendarData{
		DisplayYear:  displayYear,
		DisplayMonth: displayMonth,
		MonthName:    germanMonths[time.Month(displayMonth)],
		DiaryURL:     diaryRootURL,
		YearURL:      yearURL,
		MonthURL:     monthURLBase,
		PrevMonURL:   prevMonURL,
		NextMonURL:   nextMonURL,
		Weeks:        weeks,
		Years:        years,
		AllMonths:    allMonths,
	}

	var buf bytes.Buffer
	if err := diaryCalTmpl.Execute(&buf, data); err != nil {
		log.Printf("diary calendar template: %v", err)
		return ""
	}
	return template.HTML(buf.String())
}

// diaryPhoto is a photo file whose name starts with a YYYY-MM-DD date prefix.
type diaryPhoto struct {
	Date     time.Time
	Name     string
	URL      string
	ThumbURL string
}

type diaryMonthSummary struct {
	ID     string
	Name   string
	URL    string
	Photos []diaryPhoto
}

type diaryDaySection struct {
	ID      string
	Heading string
	URL     string
	EditURL string
	Content template.HTML
	Photos  []diaryPhoto
}

type diaryYearData struct {
	Months []diaryMonthSummary
	Year   int
}
type diaryMonthData struct{ Days []diaryDaySection }
type diaryDayData struct{ Photos []diaryPhoto }

var diaryYearTmpl = template.Must(template.ParseFS(assets, "assets/diary/diary-year.html"))
var diaryMonthTmpl = template.Must(template.ParseFS(assets, "assets/diary/diary-month.html"))
var diaryDayTmpl = template.Must(template.ParseFS(assets, "assets/diary/diary-day.html"))

var germanWeekdays = map[time.Weekday]string{
	time.Sunday:    "Sonntag",
	time.Monday:    "Montag",
	time.Tuesday:   "Dienstag",
	time.Wednesday: "Mittwoch",
	time.Thursday:  "Donnerstag",
	time.Friday:    "Freitag",
	time.Saturday:  "Samstag",
}

var germanMonths = map[time.Month]string{
	time.January:   "Januar",
	time.February:  "Februar",
	time.March:     "März",
	time.April:     "April",
	time.May:       "Mai",
	time.June:      "Juni",
	time.July:      "Juli",
	time.August:    "August",
	time.September: "September",
	time.October:   "Oktober",
	time.November:  "November",
	time.December:  "Dezember",
}

func formatGermanDate(t time.Time) string {
	return fmt.Sprintf("%s, %d. %s %d",
		germanWeekdays[t.Weekday()],
		t.Day(),
		germanMonths[t.Month()],
		t.Year(),
	)
}

var photoExts = map[string]bool{
	".jpg": true, ".jpeg": true, ".png": true, ".gif": true, ".webp": true,
}

// yearPhotos returns all photos in yearFsPath whose filename starts with
// a YYYY-MM-DD date prefix.
func yearPhotos(yearFsPath, yearURLPath string) []diaryPhoto {
	entries, err := os.ReadDir(yearFsPath)
	if err != nil {
		return nil
	}
	var photos []diaryPhoto
	for _, e := range entries {
		if e.IsDir() {
			continue
		}
		name := e.Name()
		if !photoExts[strings.ToLower(filepath.Ext(name))] {
			continue
		}
		if len(name) < 10 {
			continue
		}
		t, err := time.Parse("2006-01-02", name[:10])
		if err != nil {
			continue
		}
		photoURL := path.Join(yearURLPath, url.PathEscape(name))
		thumb := photoURL
		if hasThumbnail(name) {
			thumb = thumbURL(photoURL, 300)
		}
		photos = append(photos, diaryPhoto{
			Date:     t,
			Name:     name,
			URL:      photoURL,
			ThumbURL: thumb,
		})
	}
	return photos
}

// renderDiaryYear renders month sections with photo counts for a year folder.
func renderDiaryYear(fsPath, urlPath string) template.HTML {
	year, err := strconv.Atoi(filepath.Base(fsPath))
	if err != nil {
		return ""
	}

	photos := yearPhotos(fsPath, urlPath)

	// Collect month numbers from both subdirectories and photo filenames so
	// years that contain only photos (no diary entries) still list months.
	monthSet := map[int]bool{}
	monthDirs := map[int]string{}
	entries, _ := os.ReadDir(fsPath)
	for _, e := range entries {
		if !e.IsDir() {
			continue
		}
		n, err := strconv.Atoi(e.Name())
		if err != nil || n < 1 || n > 12 {
			continue
		}
		monthSet[n] = true
		monthDirs[n] = e.Name()
	}
	for _, p := range photos {
		if p.Date.Year() == year {
			monthSet[int(p.Date.Month())] = true
		}
	}

	monthNums := make([]int, 0, len(monthSet))
	for m := range monthSet {
		monthNums = append(monthNums, m)
	}
	sort.Ints(monthNums)

	var months []diaryMonthSummary
	for _, monthNum := range monthNums {
		var monthPhotos []diaryPhoto
		for _, p := range photos {
			if p.Date.Year() == year && int(p.Date.Month()) == monthNum {
				monthPhotos = append(monthPhotos, p)
			}
		}
		monthDate := time.Date(year, time.Month(monthNum), 1, 0, 0, 0, 0, time.UTC)
		dirName, ok := monthDirs[monthNum]
		if !ok {
			dirName = fmt.Sprintf("%02d", monthNum)
		}
		months = append(months, diaryMonthSummary{
			ID:     monthDate.Format("2006-01"),
			Name:   fmt.Sprintf("%s %d", germanMonths[monthDate.Month()], year),
			URL:    path.Join(urlPath, dirName) + "/",
			Photos: monthPhotos,
		})
	}

	var buf bytes.Buffer
	if err := diaryYearTmpl.Execute(&buf, diaryYearData{Months: months, Year: year}); err != nil {
		log.Printf("diary year template: %v", err)
		return ""
	}
	return template.HTML(buf.String())
}

// renderDiaryMonth renders a section per day, each with its markdown content
// and photos sourced from the parent year folder.
func renderDiaryMonth(fsPath, urlPath string) template.HTML {
	yearFsPath := filepath.Dir(fsPath)
	yearURLPath := parentURL(urlPath)

	year, err := strconv.Atoi(filepath.Base(yearFsPath))
	if err != nil {
		return ""
	}
	monthNum, err := strconv.Atoi(filepath.Base(fsPath))
	if err != nil || monthNum < 1 || monthNum > 12 {
		return ""
	}

	allPhotos := yearPhotos(yearFsPath, yearURLPath)
	var monthPhotos []diaryPhoto
	for _, p := range allPhotos {
		if p.Date.Year() == year && int(p.Date.Month()) == monthNum {
			monthPhotos = append(monthPhotos, p)
		}
	}

	// Collect day numbers from subdirectories and from photo filenames.
	daySet := map[int]bool{}
	dayDirs := map[int]string{} // day number → actual directory name
	entries, _ := os.ReadDir(fsPath)
	for _, e := range entries {
		if !e.IsDir() {
			continue
		}
		d, err := strconv.Atoi(e.Name())
		if err != nil || d < 1 || d > 31 {
			continue
		}
		daySet[d] = true
		dayDirs[d] = e.Name()
	}
	for _, p := range monthPhotos {
		daySet[p.Date.Day()] = true
	}

	days := make([]int, 0, len(daySet))
	for d := range daySet {
		days = append(days, d)
	}
	sort.Ints(days)

	var sections []diaryDaySection
	for _, dayNum := range days {
		date := time.Date(year, time.Month(monthNum), dayNum, 0, 0, 0, 0, time.UTC)

		heading := date.Format("2006-01-02")
		dayURL := path.Join(urlPath, fmt.Sprintf("%02d", dayNum)) + "/"
		var content template.HTML
		if dirName, ok := dayDirs[dayNum]; ok {
			dayURL = path.Join(urlPath, dirName) + "/"
			dayFsPath := filepath.Join(fsPath, dirName)
			if raw, err := os.ReadFile(filepath.Join(dayFsPath, "index.md")); err == nil && len(raw) > 0 {
				raw = stripFirstHeading(raw)
				content = renderMarkdown(raw)
			}
		}

		var photos []diaryPhoto
		for _, p := range monthPhotos {
			if p.Date.Day() == dayNum {
				photos = append(photos, p)
			}
		}

		sections = append(sections, diaryDaySection{
			ID:      date.Format("2006-01-02"),
			Heading: heading,
			URL:     dayURL,
			EditURL: dayURL + "?edit",
			Content: content,
			Photos:  photos,
		})
	}

	var buf bytes.Buffer
	if err := diaryMonthTmpl.Execute(&buf, diaryMonthData{Days: sections}); err != nil {
		log.Printf("diary month template: %v", err)
		return ""
	}
	return template.HTML(buf.String())
}

// renderDiaryDay renders the photo grid for a single day, sourcing photos
// from the grandparent year folder.
func renderDiaryDay(fsPath, urlPath string) template.HTML {
	monthFsPath := filepath.Dir(fsPath)
	yearFsPath := filepath.Dir(monthFsPath)
	yearURLPath := parentURL(parentURL(urlPath))

	year, err := strconv.Atoi(filepath.Base(yearFsPath))
	if err != nil {
		return ""
	}
	monthNum, err := strconv.Atoi(filepath.Base(monthFsPath))
	if err != nil {
		return ""
	}
	dayNum, err := strconv.Atoi(filepath.Base(fsPath))
	if err != nil {
		return ""
	}

	allPhotos := yearPhotos(yearFsPath, yearURLPath)
	var photos []diaryPhoto
	for _, p := range allPhotos {
		if p.Date.Year() == year && int(p.Date.Month()) == monthNum && p.Date.Day() == dayNum {
			photos = append(photos, p)
		}
	}

	if len(photos) == 0 {
		return ""
	}

	var buf bytes.Buffer
	if err := diaryDayTmpl.Execute(&buf, diaryDayData{Photos: photos}); err != nil {
		log.Printf("diary day template: %v", err)
		return ""
	}
	return template.HTML(buf.String())
}
