package de.tsschulz.tt_tagebuch.app.calendar

import de.tsschulz.tt_tagebuch.shared.api.models.CalendarHolidayRowDto
import de.tsschulz.tt_tagebuch.shared.api.models.ClubCalendarHolidaysEnvelope
import de.tsschulz.tt_tagebuch.shared.api.models.DiaryDate
import de.tsschulz.tt_tagebuch.shared.api.models.InternalTournamentSummaryDto
import de.tsschulz.tt_tagebuch.shared.api.models.OfficialParticipationBucketDto
import de.tsschulz.tt_tagebuch.shared.api.models.ScheduleMatchDto
import de.tsschulz.tt_tagebuch.shared.api.models.TrainingCancellationDto
import de.tsschulz.tt_tagebuch.shared.api.models.TrainingGroupDto
import de.tsschulz.tt_tagebuch.shared.api.models.TrainingTimeDto
import java.time.DayOfWeek
import java.time.LocalDate
import java.time.LocalTime
import java.time.YearMonth
import java.time.format.DateTimeFormatter
import java.time.format.DateTimeParseException

private val isoFmt: DateTimeFormatter = DateTimeFormatter.ISO_LOCAL_DATE
private val dmyFmt: DateTimeFormatter = DateTimeFormatter.ofPattern("d.M.yyyy")

enum class CalendarEventType {
    Training,
    Tournament,
    OfficialTournament,
    Match,
    FriendlyMatch,
    Holiday,
    SchoolHoliday,
    TrainingCancellation,
}

enum class CalendarEventAction {
    OpenDiaryDate,
    OpenDiaryTab,
    OpenSchedule,
    OpenTournaments,
    OpenOfficialParticipations,
    None,
}

data class CalendarUiEvent(
    val id: String,
    val type: CalendarEventType,
    val date: LocalDate,
    val endDate: LocalDate = date,
    val timeLabel: String,
    val title: String,
    val subtitle: String,
    val startsAtSort: Long,
    val action: CalendarEventAction = CalendarEventAction.None,
    val diaryDateId: Int? = null,
    val tournamentId: Int? = null,
    val matchId: Int? = null,
    val cancellationId: Int? = null,
    val isRecurringTraining: Boolean = false,
) {
    val dateIso: String get() = date.format(isoFmt)
    val endDateIso: String get() = endDate.format(isoFmt)
}

private fun parseIso(s: String?): LocalDate? = try {
    if (s.isNullOrBlank()) null
    else LocalDate.parse(s.trim().take(10), isoFmt)
} catch (_: DateTimeParseException) {
    null
}

private fun parseDmy(s: String?): LocalDate? = try {
    if (s.isNullOrBlank()) null
    else LocalDate.parse(s.trim(), dmyFmt)
} catch (_: DateTimeParseException) {
    null
}

private fun minutesFromTime(time: String?): Int {
    val t = time?.trim()?.take(5) ?: return 0
    val parts = t.split(':')
    val h = parts.getOrNull(0)?.toIntOrNull() ?: 0
    val m = parts.getOrNull(1)?.toIntOrNull() ?: 0
    return h.coerceIn(0, 23) * 60 + m.coerceIn(0, 59)
}

private fun combineSortKey(d: LocalDate, time: String?): Long {
    val day = d.toEpochDay()
    return day * 1440L + minutesFromTime(time)
}

private fun formatTimeRange(start: String?, end: String?): String {
    val a = start?.trim()?.take(5) ?: ""
    val b = end?.trim()?.take(5) ?: ""
    return when {
        a.isNotEmpty() && b.isNotEmpty() -> "$a–$b"
        a.isNotEmpty() -> a
        b.isNotEmpty() -> b
        else -> ""
    }
}

private fun formatTime(time: String?): String = time?.trim()?.take(5) ?: ""

/** JS Date.getDay(): 0 = Sunday … 6 = Saturday */
private fun jsWeekday(d: LocalDate): Int {
    val dow = d.dayOfWeek
    return when (dow) {
        DayOfWeek.SUNDAY -> 0
        DayOfWeek.MONDAY -> 1
        DayOfWeek.TUESDAY -> 2
        DayOfWeek.WEDNESDAY -> 3
        DayOfWeek.THURSDAY -> 4
        DayOfWeek.FRIDAY -> 5
        DayOfWeek.SATURDAY -> 6
    }
}

private fun normalizeTrainingTimeKey(time: String): String =
    time.replace('\u2013', '-').replace('\u2012', '-').replace('\u2212', '-')
        .replace(" ", "")
        .filter { it != '\u00a0' }

private fun genericTitle(t: String) = t.isBlank() || t.trim().equals("training", ignoreCase = true)

object CalendarAggregator {

    fun fromDiaryDates(diary: List<DiaryDate>, displayedYear: Int): List<CalendarUiEvent> =
        diary.mapNotNull { entry ->
            val d = parseIso(entry.date) ?: return@mapNotNull null
            if (d.year != displayedYear) return@mapNotNull null
            val timeLabel = formatTimeRange(entry.trainingStart, entry.trainingEnd)
            val tags = entry.diaryTags.joinToString(", ") { it.name }
            CalendarUiEvent(
                id = "training-diary-${entry.id}",
                type = CalendarEventType.Training,
                date = d,
                timeLabel = timeLabel,
                title = "Training",
                subtitle = tags,
                startsAtSort = combineSortKey(d, entry.trainingStart),
                action = CalendarEventAction.OpenDiaryDate,
                diaryDateId = entry.id,
                isRecurringTraining = false,
            )
        }

    fun fromRecurringTraining(groups: List<TrainingGroupDto>, displayedYear: Int): List<CalendarUiEvent> {
        val out = ArrayList<CalendarUiEvent>()
        for (group in groups) {
            for (time in group.trainingTimes) {
                out.addAll(createRecurringTrainingForSlot(group, time, displayedYear))
            }
        }
        return out
    }

    private fun createRecurringTrainingForSlot(group: TrainingGroupDto, time: TrainingTimeDto, year: Int): List<CalendarUiEvent> {
        val weekday = time.weekday
        if (weekday !in 0..6 || time.startTime.isBlank()) return emptyList()
        val jan1 = LocalDate.of(year, 1, 1)
        val w0 = jsWeekday(jan1)
        var firstOffset = (weekday - w0 + 7) % 7
        var cursor = jan1.plusDays(firstOffset.toLong())
        val list = ArrayList<CalendarUiEvent>()
        while (cursor.year == year) {
            val timeLabel = formatTimeRange(time.startTime, time.endTime)
            list.add(
                CalendarUiEvent(
                    id = "training-time-${group.id}-${time.id}-${cursor.format(isoFmt)}",
                    type = CalendarEventType.Training,
                    date = cursor,
                    timeLabel = timeLabel,
                    title = group.name.ifBlank { "Training" },
                    subtitle = "Regelmäßige Trainingszeit",
                    startsAtSort = combineSortKey(cursor, time.startTime),
                    action = CalendarEventAction.OpenDiaryTab,
                    isRecurringTraining = true,
                ),
            )
            cursor = cursor.plusWeeks(1)
        }
        return list
    }

    fun fromCancellations(rows: List<TrainingCancellationDto>): List<CalendarUiEvent> =
        rows.mapNotNull { c ->
            val start = parseIso(c.startDate ?: c.date) ?: return@mapNotNull null
            val end = parseIso(c.endDate ?: c.startDate ?: c.date) ?: start
            CalendarUiEvent(
                id = "training-cancellation-${c.id}",
                type = CalendarEventType.TrainingCancellation,
                date = start,
                endDate = end,
                timeLabel = "",
                title = c.reason?.trim()?.ifBlank { null } ?: "Training fällt aus",
                subtitle = "Trainingsausfall",
                startsAtSort = combineSortKey(start, null),
                cancellationId = c.id,
                action = CalendarEventAction.None,
            )
        }

    fun fromTournaments(rows: List<InternalTournamentSummaryDto>): List<CalendarUiEvent> =
        rows.mapNotNull { t ->
            val d = parseIso(t.date) ?: return@mapNotNull null
            CalendarUiEvent(
                id = "tournament-${t.id}",
                type = CalendarEventType.Tournament,
                date = d,
                timeLabel = "",
                title = t.name?.trim()?.ifBlank { null } ?: "Turnier",
                subtitle = if (t.allowsExternal == true) "Offenes Turnier" else "Vereinsturnier",
                startsAtSort = combineSortKey(d, null),
                tournamentId = t.id,
                action = CalendarEventAction.OpenTournaments,
            )
        }

    fun fromMatches(matches: List<ScheduleMatchDto>): List<CalendarUiEvent> =
        matches.mapNotNull { m ->
            val d = parseIso(m.date) ?: return@mapNotNull null
            val home = m.homeTeam?.name?.ifBlank { null } ?: "Heim"
            val guest = m.guestTeam?.name?.ifBlank { null } ?: "Gast"
            CalendarUiEvent(
                id = "match-${m.id}",
                type = CalendarEventType.Match,
                date = d,
                timeLabel = formatTime(m.time),
                title = "$home – $guest",
                subtitle = m.leagueDetails?.name?.ifBlank { null } ?: "Punktspiel",
                startsAtSort = combineSortKey(d, m.time),
                matchId = m.id,
                action = CalendarEventAction.OpenSchedule,
            )
        }

    fun fromFriendlyMatches(matches: List<ScheduleMatchDto>): List<CalendarUiEvent> =
        matches.mapNotNull { m ->
            val d = parseIso(m.date) ?: return@mapNotNull null
            val home = m.homeTeam?.name?.ifBlank { null } ?: "Heim"
            val guest = m.guestTeam?.name?.ifBlank { null } ?: "Gast"
            CalendarUiEvent(
                id = "friendly-match-${m.friendlyMatchId ?: m.id}",
                type = CalendarEventType.FriendlyMatch,
                date = d,
                timeLabel = formatTime(m.time),
                title = "$home – $guest",
                subtitle = "Freundschaftsspiel",
                startsAtSort = combineSortKey(d, m.time),
                matchId = m.id,
                action = CalendarEventAction.OpenSchedule,
            )
        }

    fun fromOfficialParticipations(buckets: List<OfficialParticipationBucketDto>): List<CalendarUiEvent> =
        buckets.mapNotNull { t ->
            if (t.entries.isEmpty()) return@mapNotNull null
            val date = parseDmy(t.startDate) ?: parseIso(t.startDate) ?: return@mapNotNull null
            val end = parseDmy(t.endDate) ?: parseIso(t.endDate) ?: date
            val distinctMembers = t.entries.mapNotNull { it.memberId }.toSet().size
            val subtitle = if (distinctMembers > 0) "$distinctMembers Teilnehmer" else "${t.entries.size} Starts"
            CalendarUiEvent(
                id = "official-tournament-${t.tournamentId ?: t.title}",
                type = CalendarEventType.OfficialTournament,
                date = date,
                endDate = end,
                timeLabel = "",
                title = t.title?.trim()?.ifBlank { null } ?: "Turnierteilnahme",
                subtitle = subtitle,
                startsAtSort = combineSortKey(date, null),
                action = CalendarEventAction.OpenOfficialParticipations,
            )
        }

    fun fromHolidays(env: ClubCalendarHolidaysEnvelope): List<CalendarUiEvent> {
        val h = env.holidays.mapNotNull { mapHolidayRow(it, CalendarEventType.Holiday, "Feiertag") }
        val s = env.schoolHolidays.mapNotNull { mapHolidayRow(it, CalendarEventType.SchoolHoliday, "Schulferien") }
        return h + s
    }

    private fun mapHolidayRow(entry: CalendarHolidayRowDto, type: CalendarEventType, fallback: String): CalendarUiEvent? {
        val start = parseIso(entry.startDate) ?: return null
        val end = parseIso(entry.endDate) ?: start
        val name = entry.name?.trim()?.ifBlank { null } ?: fallback
        return CalendarUiEvent(
            id = "${type.name.lowercase()}-${entry.id ?: "${start.format(isoFmt)}-$name"}",
            type = type,
            date = start,
            endDate = end,
            timeLabel = "",
            title = name,
            subtitle = fallback,
            startsAtSort = combineSortKey(start, null),
            action = CalendarEventAction.None,
        )
    }

    fun mergeRecurringTrainingSlots(events: List<CalendarUiEvent>): List<CalendarUiEvent> {
        val slotMap = LinkedHashMap<String, MutableList<CalendarUiEvent>>()
        val passthrough = ArrayList<CalendarUiEvent>()
        for (e in events) {
            if (e.type != CalendarEventType.Training || e.timeLabel.isBlank()) {
                passthrough.add(e)
                continue
            }
            val timeKey = normalizeTrainingTimeKey(e.timeLabel)
            if (timeKey.isEmpty()) {
                passthrough.add(e)
                continue
            }
            val key = "${e.date.format(isoFmt)}|$timeKey"
            slotMap.getOrPut(key) { mutableListOf() }.add(e)
        }
        val merged = ArrayList<CalendarUiEvent>()
        for ((slotKey, list) in slotMap) {
            if (list.size == 1) {
                merged.add(list[0])
                continue
            }
            val sorted = list.sortedBy { it.startsAtSort }
            val base = sorted.first()
            val rawTitles = sorted.map { it.title.trim() }.filter { it.isNotEmpty() }.distinct()
            val specific = rawTitles.filter { !genericTitle(it) }
            val titleJoined = if (specific.isNotEmpty()) specific.joinToString(" · ") else rawTitles.joinToString(" · ").ifBlank { base.title }
            val safeIdKey = slotKey.replace('|', '-')
            val hasRecurring = sorted.any { it.isRecurringTraining }
            val otherSubtitles = sorted.map { it.subtitle.trim() }.filter { it.isNotEmpty() && it != "Regelmäßige Trainingszeit" }.distinct()
            val subtitleJoined = if (hasRecurring) {
                if (otherSubtitles.isNotEmpty()) "Regelmäßige Trainingszeit · ${otherSubtitles.joinToString(" · ")}"
                else "Regelmäßige Trainingszeit"
            } else {
                otherSubtitles.joinToString(" · ").ifBlank { base.subtitle }
            }
            merged.add(
                base.copy(
                    id = "training-merged-$safeIdKey",
                    title = titleJoined,
                    subtitle = subtitleJoined,
                    startsAtSort = sorted.minOf { it.startsAtSort },
                    isRecurringTraining = hasRecurring,
                ),
            )
        }
        return passthrough + merged
    }

    private fun expandCancellationDates(start: LocalDate, end: LocalDate): List<LocalDate> {
        val out = ArrayList<LocalDate>()
        var cur = start
        var guard = 0
        while (!cur.isAfter(end) && guard++ < 800) {
            out.add(cur)
            cur = cur.plusDays(1)
        }
        return out
    }

    fun applyTrainingCancellationsToRecurring(events: List<CalendarUiEvent>): List<CalendarUiEvent> {
        val cancellationDates = events
            .filter { it.type == CalendarEventType.TrainingCancellation }
            .flatMap { expandCancellationDates(it.date, it.endDate) }
            .map { it.format(isoFmt) }
            .toSet()
        return events.filter { e ->
            if (e.type != CalendarEventType.Training || !e.isRecurringTraining) return@filter true
            !cancellationDates.contains(e.date.format(isoFmt))
        }
    }

    fun buildAll(
        displayedYear: Int,
        diary: List<DiaryDate>,
        trainingGroups: List<TrainingGroupDto>,
        cancellations: List<TrainingCancellationDto>,
        tournaments: List<InternalTournamentSummaryDto>,
        matches: List<ScheduleMatchDto>,
        friendlyMatches: List<ScheduleMatchDto>,
        official: List<OfficialParticipationBucketDto>,
        holidays: ClubCalendarHolidaysEnvelope,
    ): List<CalendarUiEvent> {
        val raw = ArrayList<CalendarUiEvent>()
        raw.addAll(fromDiaryDates(diary, displayedYear))
        raw.addAll(fromRecurringTraining(trainingGroups, displayedYear))
        raw.addAll(fromCancellations(cancellations))
        raw.addAll(fromTournaments(tournaments))
        raw.addAll(fromMatches(matches))
        raw.addAll(fromFriendlyMatches(friendlyMatches))
        raw.addAll(fromOfficialParticipations(official))
        raw.addAll(fromHolidays(holidays))
        val afterCancel = applyTrainingCancellationsToRecurring(raw)
        return mergeRecurringTrainingSlots(afterCancel)
    }

    fun eventsForDay(events: List<CalendarUiEvent>, day: LocalDate): List<CalendarUiEvent> =
        events.filter { e ->
            !day.isBefore(e.date) && !day.isAfter(e.endDate)
        }.sortedBy { it.startsAtSort }

    fun eventsInMonth(events: List<CalendarUiEvent>, yearMonth: YearMonth): List<CalendarUiEvent> {
        val start = yearMonth.atDay(1)
        val end = yearMonth.atEndOfMonth()
        return events.filter { e ->
            !e.date.isAfter(end) && !e.endDate.isBefore(start)
        }.sortedBy { it.startsAtSort }
    }

    fun monthGridStart(yearMonth: YearMonth): LocalDate {
        val first = yearMonth.atDay(1)
        val wd = jsWeekday(first)
        val back = (wd + 6) % 7
        return first.minusDays(back.toLong())
    }

    fun monthGridCells(gridStart: LocalDate): List<LocalDate> =
        (0 until 42).map { gridStart.plusDays(it.toLong()) }
}
