"""Chronological study-session report; all spans use [start, end)."""


def merge_sessions(left, right):
    """Stable two-pointer merge. Equal timestamps prefer the left input."""
    for batch in (left, right):
        if any(len(row) != 3 or not isinstance(row[0], int)
               or not isinstance(row[1], str) or not row[1]
               or not isinstance(row[2], int) for row in batch):
            raise ValueError("sessions must be (integer time, nonempty tag, integer score)")
        if any(batch[i][0] > batch[i + 1][0] for i in range(len(batch) - 1)):
            raise ValueError("each batch must be sorted by timestamp")
    merged, i, j = [], 0, 0
    while i < len(left) and j < len(right):
        if left[i][0] <= right[j][0]:
            merged.append(left[i]); i += 1
        else:
            merged.append(right[j]); j += 1
    merged.extend(left[i:]); merged.extend(right[j:])
    return merged


def prefix_scores(scores):
    prefix = [0]
    for score in scores:
        prefix.append(prefix[-1] + score)
    return prefix


def range_scores(prefix, queries):
    result = []
    for start, end in queries:
        if not 0 <= start <= end < len(prefix):
            raise ValueError("query must satisfy 0 <= start <= end <= number of sessions")
        result.append(prefix[end] - prefix[start])
    return result


def longest_unique_span(tags):
    """Longest span with distinct tags; ties prefer the earliest start."""
    last, start, best = {}, 0, (0, 0)
    for end, tag in enumerate(tags):
        start = max(start, last.get(tag, -1) + 1)
        last[tag] = end
        if end + 1 - start > best[1] - best[0]:
            best = (start, end + 1)
    return best


def best_score_span(scores):
    """Maximum nonempty subarray; ties prefer earliest start, then earliest end."""
    if not scores:
        return None
    current = best_sum = scores[0]
    start = 0
    best = (0, 1)
    for index in range(1, len(scores)):
        if current < 0:
            current, start = scores[index], index
        else:
            current += scores[index]
        candidate = (start, index + 1)
        if current > best_sum or (current == best_sum and candidate < best):
            best_sum, best = current, candidate
    return {"score": best_sum, "span": best}


def session_report(left, right, queries=()):
    sessions = merge_sessions(left, right)
    scores = [row[2] for row in sessions]
    return {
        "sessions": sessions,
        "range_scores": range_scores(prefix_scores(scores), queries),
        "unique_span": longest_unique_span([row[1] for row in sessions]),
        "best_score": best_score_span(scores),
    }
