package api

import "strings"

// Where a title's closing credits begin.
//
// Two sources, in order of trust, because Emby gives one and the media gives the other:
//
//   - `CreditsStart`, a marker Emby's own detector writes. It is in Emby's `MarkerType`
//     enumeration and is what this feature was originally built on — but **Emby 4.10 does not
//     write it**. A survey of a 20,000-item library found `Chapter`, `IntroStart` and
//     `IntroEnd` and nothing else, so the enum value existing is not the detector populating
//     it. It is still read first, so the day a version does write it this needs no change.
//   - A chapter *named* like credits. Plenty of media carries "Credits" or "End Credits" as
//     ordinary chapter metadata, and in that same library 216 items had one, clustered at
//     90–98% of runtime and consistent within a show. That is where the feature's coverage
//     actually comes from today.
//
// There is deliberately no end marker in either source. Credits run to the end of the file by
// definition, so nothing writes one and this must not invent one.

const markerCreditsStart = "CreditsStart"

// creditsMinimumPositionFraction is how far into a file a credit roll has to begin.
//
// **This is the load-bearing guard, and it exists because of one observed case.** Chapter
// names are not a vocabulary anybody agreed on, and real media carries "Opening Credits" —
// Belfast at 1% of runtime, Game of Thrones at 0%. A name match without a position test
// therefore starts the credits pane in the *first minute* of a film and runs its opening at
// double speed, which is the worst thing this feature could possibly do.
//
// Three quarters is deliberately far below the evidence rather than near it: every genuine
// credit roll in that survey began at 90% or later, so this leaves fifteen points of headroom
// for a long roll while rejecting the whole first half of a file outright.
const creditsMinimumPositionFraction = 0.75

// creditsFromChapters finds where the closing credits begin.
//
// The rule exists twice — the television's copy is `creditsStartFrom` in `data/Credits.kt` —
// and the two are pinned by deliberately parallel tests (`credits_test.go`, `CreditsTest`).
// With no gateway there is nobody to ask, and the picture must not start shrinking at a
// different moment depending on whether the container is up.
//
// Most of this is about refusing to answer, and nothing is a perfectly good answer: the player
// never shrinks anything and the credits play out full size, which is what every other client
// does anyway.
//
// [runtimeMs] may be zero when Emby does not report one. An explicit marker is still honoured
// then — it is Emby asserting a position rather than this inferring one — but a *named*
// chapter is refused outright, because the name alone cannot distinguish an opening credit
// sequence from a closing one and the position test is the only thing that can.
func creditsFromChapters(chapters []embyChapter, runtimeMs int64) (int64, bool) {
	floor := int64(-1)
	if runtimeMs > 0 {
		floor = int64(float64(runtimeMs) * creditsMinimumPositionFraction)
	}

	// An explicit marker first. The last one wins, where the intro rule takes the first:
	// two starts mean the markers are untrustworthy, so each rule picks whichever risks
	// least, and the two features are damaged in opposite directions. An intro skip firing
	// late throws somebody past the story, so the earlier marker is safer there; the credits
	// pane firing early runs the last scene past them at double speed, so the later marker is
	// safer here.
	marked := int64(-1)
	for _, chapter := range chapters {
		if chapter.MarkerType != markerCreditsStart || chapter.StartPositionTicks <= 0 {
			continue
		}
		marked = chapter.StartPositionTicks / ticksPerMillisecond
	}
	// A marker below the floor is a mis-detection whoever wrote it, so it falls through to the
	// names rather than being honoured — but with no runtime to measure against, an explicit
	// assertion gets the benefit of the doubt.
	if marked > 0 && (floor < 0 || marked >= floor) {
		return marked, true
	}

	if floor < 0 {
		return 0, false
	}

	// Then the names. The *earliest* qualifying chapter wins here, which is the opposite of
	// the marker rule above and is not an inconsistency: several credits-named chapters are
	// ordinary rather than suspicious — "The Pitt" carries both "Credits" and "End Credits" —
	// and they describe one roll, which begins at the first of them.
	named := int64(-1)
	for _, chapter := range chapters {
		if !isCreditsChapterName(chapter.Name) || chapter.StartPositionTicks <= 0 {
			continue
		}
		at := chapter.StartPositionTicks / ticksPerMillisecond
		if at < floor {
			continue
		}
		if named < 0 || at < named {
			named = at
		}
	}
	if named > 0 {
		return named, true
	}
	return 0, false
}

// isCreditsChapterName recognises a chapter that names a credit roll.
//
// The exclusions are belt-and-braces beside [creditsMinimumPositionFraction], which is what
// actually stops an opening sequence being read as a closing one — a position test catches
// wordings nobody thought of, where a list of them only catches the ones on the list. They are
// here so the trap is stated where the next reader will look for it.
func isCreditsChapterName(name string) bool {
	lowered := strings.ToLower(strings.TrimSpace(name))
	if lowered == "" {
		return false
	}
	for _, opening := range []string{"opening", "main title", "title sequence", "intro"} {
		if strings.Contains(lowered, opening) {
			return false
		}
	}
	return strings.Contains(lowered, "credit") ||
		strings.Contains(lowered, "end titles") ||
		strings.Contains(lowered, "closing")
}
