#!/usr/bin/env python3
"""Independently inspect these two MIDI fixtures with mido; no encoder imports."""
from pathlib import Path
from importlib.metadata import version
from datetime import datetime, timezone
import hashlib
import json
import struct

import mido

HERE = Path(__file__).resolve().parent
EXPORTER_SHA = 'ce7336d07f8cd4e21fc1eb9f244f207b6db58693a3e015ef69ec392214dc4e79'
EXPECTED_NOTES = [
    {'channel': 0, 'pitch': 60, 'start_tick': 480, 'end_tick': 1440, 'velocity': 80},
    {'channel': 0, 'pitch': 64, 'start_tick': 1920, 'end_tick': 2880, 'velocity': 80},
]


def require(condition, reason):
    if not condition:
        raise ValueError(reason)


def sha(path):
    return hashlib.sha256(path.read_bytes()).hexdigest()


def parse(filename, expected_eot):
    path = HERE / filename
    raw = path.read_bytes()
    require(len(raw) >= 22 and raw[:4] == b'MThd', 'Missing SMF header')
    header = struct.unpack('>IHHH', raw[4:14])
    require(header == (6, 0, 1, 960), 'Expected SMF0, one track, 960 PPQ')
    require(raw[14:18] == b'MTrk', 'Missing MIDI track')
    require(struct.unpack('>I', raw[18:22])[0] == len(raw)-22, 'Track size must cover the exact complete file')
    midi = mido.MidiFile(path)
    require((midi.type, len(midi.tracks), midi.ticks_per_beat) == (0, 1, 960), 'Parsed header mismatch')
    tick = 0
    active = {}
    notes = []
    events = []
    tempos = []
    eots = []
    last_off = None
    for index, message in enumerate(midi.tracks[0]):
        require(isinstance(message.time, int) and message.time >= 0, 'Invalid delta ticks')
        tick += message.time
        require(message.type in ('set_tempo', 'program_change', 'note_on', 'note_off', 'end_of_track'), 'Unexpected event in the control fixture')
        payload = message.dict()
        delta = payload.pop('time')
        events.append({'absolute_tick': tick, 'delta_tick': delta, **payload})
        if message.type == 'set_tempo':
            tempos.append({'tick': tick, 'microseconds_per_beat': message.tempo})
        elif message.type == 'end_of_track':
            eots.append(tick)
            require(index == len(midi.tracks[0])-1, 'Events after End-of-Track')
        elif message.type in ('note_on', 'note_off'):
            key = (message.channel, message.note)
            if message.type == 'note_on' and message.velocity > 0:
                require(key not in active, 'Overlapping same-pitch note-on')
                active[key] = (tick, message.velocity)
            else:
                require(key in active, 'Orphan note-off')
                start, velocity = active.pop(key)
                require(tick > start, 'Nonpositive note duration')
                last_off = tick
                notes.append({'channel': message.channel, 'pitch': message.note,
                    'start_tick': start, 'end_tick': tick, 'velocity': velocity})
    require(not active, 'Hanging note-on')
    require(tempos == [{'tick': 0, 'microseconds_per_beat': 500000}], 'Expected fixed tempo at zero')
    require(eots == [expected_eot], 'Unexpected End-of-Track tick')
    require(notes == EXPECTED_NOTES, 'Actual note events differ from the fixed independent tick expectations')
    require(last_off == 2880 and expected_eot >= last_off, 'Unexpected last note-off')
    seconds = lambda ticks: mido.tick2second(ticks, 960, 500000)
    return {'status': 'passed', 'file': filename, 'bytes': len(raw), 'sha256': sha(path),
        'smf_type': midi.type, 'tracks': 1, 'ppq': 960, 'tempos': tempos,
        'note_count': len(notes), 'notes': notes, 'events': events,
        'last_note_off_tick': last_off, 'last_note_off_seconds': seconds(last_off),
        'end_of_track_tick': expected_eot, 'end_of_track_seconds': seconds(expected_eot),
        'note_free_track_tail_ticks': expected_eot-last_off,
        'note_free_track_tail_seconds': seconds(expected_eot-last_off),
        'strict_checks': {'exact_track_chunk_length': True, 'positive_paired_notes': True,
            'orphan_note_off_count': 0, 'hanging_note_on_count': 0,
            'same_pitch_overlap_count': 0, 'single_final_end_of_track': True}}


def main():
    require(version('mido') == '1.3.3', 'Expected the existing mido 1.3.3 runtime')
    exporter = HERE/'midi-file.js'
    require(sha(exporter) == EXPORTER_SHA, 'Frozen exporter copy hash differs')
    files = [parse('same-notes-1.5s.mid', 2880), parse('same-notes-4.0s.mid', 7680)]
    without_eot = lambda item: [event for event in item['events'] if event['type'] != 'end_of_track']
    require(without_eot(files[0]) == without_eot(files[1]), 'Non-EOT events changed')
    result = {'status': 'passed_internal_synthetic_export_control',
        'recorded_at': datetime.now(timezone.utc).isoformat(), 'mido_version': version('mido'),
        'scope': 'Pure-exporter self-authored CC0 note control; no audio input, browser conversion, DAW import or acoustic tail test.',
        'source_provenance': {'original': 'dist/midi-file.js', 'frozen_copy': 'midi-file.js',
            'sha256': EXPORTER_SHA, 'source_release': '780d9051-28ca-4d8f-81b0-6deb08db54a3',
            'generator': 'generate.cjs', 'generator_sha256': sha(HERE/'generate.cjs'),
            'independent_parser': 'verify.py', 'independent_parser_sha256': sha(Path(__file__)),
            'parser_calls_or_imports_encoder': False},
        'input_notes': [{'pitch': 60, 'start': 0.25, 'end': 0.75, 'velocity': 80},
            {'pitch': 64, 'start': 1.0, 'end': 1.5, 'velocity': 80}],
        'only_generator_input_changed': 'duration: 1.5 seconds versus 4.0 seconds',
        'files': files, 'comparison': {'non_eot_events_identical': True,
            'identical_non_eot_event_count': len(without_eot(files[0])),
            'last_note_off_identical': True, 'end_of_track_delta_ticks': 4800,
            'end_of_track_delta_seconds': 2.5,
            'raw_files_identical': False,
            'raw_byte_difference_explanation': 'The End-of-Track delta-time and its encoded byte length change; the track chunk length changes accordingly.'},
        'limits': ['Track event duration and note-free trailing time are not acoustic instrument release or rendered audio silence.',
            'No audio transcription, app/DAW import, playback, listening, browser or device test was performed.',
            'Internal candidate material only; not a new product page, exclusive capability claim or verified common search-results gap.',
            'CC0 applies to the self-authored note fixture/generated MIDI material; the copied exporter retains its project provenance.']}
    (HERE/'measurements.json').write_text(json.dumps(result, ensure_ascii=False, indent=2)+'\n')
    print('passed: identical 6 non-EOT events; last note-off=2880; EOT=2880/7680; note-free tail=0/2.5s')


if __name__ == '__main__':
    main()
