/* ── VERIFY DROP ZONE ──
   Faithful browser-side EPI verification matching the Python codebase:
   - Container detection (legacy ZIP + envelope-v2 with <!-- magic)
   - mimetype validation (application/vnd.epi+zip)
   - File manifest SHA-256 integrity check
   - Ed25519 signature verification via Web Crypto API
   - Structured report: facts + identity + metadata (matching create_verification_report) */
var dz=document.getElementById('dropZoneEl'),fi=document.getElementById('fileInput'),dr=document.getElementById('dropResult');
var EPI_MIMETYPE='application/vnd.epi+zip',EPI_ENVELOPE_MAGIC='<!--';

function reportDOM(report){
  var f=report.facts,i=report.identity,m=report.metadata;
  var trustColor='var(--verified)',trustIcon='&#10003;';
  if(!f.integrity_ok||f.signature_valid===false){trustColor='var(--tamper)';trustIcon='&#10007;'}
  else if(!f.has_signature||i.status==='UNKNOWN'){trustColor='var(--warn)';trustIcon='&#9888;'}

  var steps=m.steps_count!==null?(' · '+m.steps_count+' steps'):'';
  var chks=[];
  if(f.structure_ok)chks.push('Structure valid');
  if(f.integrity_ok)chks.push('Hashes match');
  else chks.push('<span style="color:var(--tamper)">Hashes mismatch ('+Object.keys(f.mismatches||{}).length+')</span>');
  if(f.has_signature){
    if(f.signature_valid===true)chks.push('Signature: VALID (Ed25519)');
    else if(f.signature_valid===false)chks.push('<span style="color:var(--tamper)">Signature: INVALID</span>');
    else chks.push('Signature: UNVERIFIED (browser lacks Ed25519)');
  }else{chks.push('Unsigned')}
  chks.push('Chain: '+(f.chain_ok!==false?'intact':'<span style="color:var(--tamper)">broken</span>'));
  chks.push('Identity: '+i.status);

  return '<div style="font-size:0.82rem;margin-bottom:0.6rem"><strong style="font-size:1.05rem;color:'+trustColor+'">'+trustIcon+' '+report.trust_level+' TRUST</strong> <span style="color:var(--ink-muted);font-size:0.72rem">v'+m.spec_version+steps+'</span></div>'
    +chks.join('<br>')
    +'<div style="margin-top:0.5rem;font-size:0.68rem;color:var(--ink-dim);border-top:1px solid var(--border);padding-top:0.5rem">'+report.trust_message+'</div>'
    +(i.detail?'<div style="margin-top:0.3rem;font-size:0.68rem;color:var(--ink-dim)">'+i.detail+'</div>':'');
}

function updateChecks(report){
  function set(id,pass,text){var el=document.getElementById(id);if(!el)return;if(pass)el.classList.add('pass');el.innerHTML='<span class="check-dot"></span> '+text}
  var f=report.facts;
  set('chk1',f.structure_ok,'01 · Structure '+(f.structure_ok?'Valid — valid EPI archive':'<span style="color:var(--tamper)">Invalid</span>'));
  set('chk2',f.integrity_ok,'02 · Integrity '+(f.integrity_ok?'PASS — all file hashes match':'<span style="color:var(--tamper)">FAIL — '+Object.keys(f.mismatches||{}).length+' mismatch(es)</span>'));
  if(f.has_signature){
    // Only mark pass when crypto actually returned true — null is not a green check
    set('chk3',f.signature_valid===true,'03 · Signature '+(f.signature_valid===true?'VALID — Ed25519 verified':f.signature_valid===null?'CHECK — browser Ed25519 limited (use epi verify)':'<span style="color:var(--tamper)">INVALID — tampered</span>'));
  }else{set('chk3',false,'03 · Signature — none present (unsigned)')}
  set('chk4',f.chain_ok!==false,'04 · Chain '+(f.chain_ok!==false?'INTACT':'<span style="color:var(--tamper)">BROKEN</span>'));
  set('chk5',f.has_signature,'05 · Identity — '+report.identity.status+(report.identity.name?' ('+report.identity.name+')':''));
}

async function sha256(buf){var h=await crypto.subtle.digest('SHA-256',buf);return Array.from(new Uint8Array(h)).map(function(b){return b.toString(16).padStart(2,'0')}).join('')}
function buf2hex(buf){return Array.from(new Uint8Array(buf)).map(function(b){return b.toString(16).padStart(2,'0')}).join('')}

async function deriveKeyName(pubKeyHex){
  var hash=await sha256(new TextEncoder().encode(pubKeyHex));
  return hash.substring(0,16);
}

async function verifyEd25519(sigStr,pubKeyHex,hashHex){
  try{
    var parts=sigStr.split(':');
    if(parts.length!==3||parts[0]!=='ed25519')return{valid:false,msg:'Unsupported signature: '+sigStr.substring(0,20)+'...'};
    var rawSigHex=parts[2];
    var sigBytes=new Uint8Array(rawSigHex.match(/.{1,2}/g).map(function(b){return parseInt(b,16)}));
    var pubBytes=new Uint8Array(pubKeyHex.match(/.{1,2}/g).map(function(b){return parseInt(b,16)}));
    if(pubBytes.length!==32)return{valid:false,msg:'Invalid public key length: '+pubBytes.length};
    // Cryptographically bind the key name to the public key (matches Python)
    var expectedKeyName=await deriveKeyName(pubKeyHex);
    if(parts[1]!==expectedKeyName)return{valid:false,msg:'Key name does not match public key'};
    var hashBytes=new Uint8Array(hashHex.match(/.{1,2}/g).map(function(b){return parseInt(b,16)}));
    var key=await crypto.subtle.importKey('raw',pubBytes,{name:'Ed25519'},false,['verify']);
    var ok=await crypto.subtle.verify({name:'Ed25519'},key,sigBytes,hashBytes);
    return{valid:ok,msg:ok?'Signature valid':'Signature invalid — data may be tampered'};
  }catch(e){return{valid:null,msg:'Ed25519 error: '+e.message}}
}

function detectContainer(buffer){
  var prefix=new TextDecoder().decode(buffer.slice(0,4));
  // Envelope-v2 files are polyglot HTML: they start with the comment magic "<!--"
  if(prefix==='<!--')return'envelope-v2';
  var sig=new Uint8Array(buffer.slice(0,4));
  if(sig[0]===0x50&&sig[1]===0x4B&&sig[2]===0x03&&sig[3]===0x04)return'legacy-zip';
  return null;
}

function extractZIPPayload(buffer,fmt){
  if(fmt==='legacy-zip')return buffer;
  var view=new Uint8Array(buffer);
  // EPI1 envelope: skip binary header to find ZIP PK marker
  var marker=[0x50,0x4B,0x03,0x04];
  var start=Math.max(128, view.indexOf(marker[0]));
  for(var i=start;i<view.length-4;i++){if(view[i]===marker[0]&&view[i+1]===marker[1]&&view[i+2]===marker[2]&&view[i+3]===marker[3])return buffer.slice(i)}
  return buffer.slice(128);
}

/*
  Canonical JSON encoder that matches Python json.dumps(sort_keys=True,
  separators=(',',':'), ensure_ascii=False).

  We also preserve the original text for every JSON number token.  Python keeps
  the trailing ".0" for floats (e.g. 900.0) while JavaScript's JSON.stringify
  drops it (900).  Without preservation, manifests/steps that contain float
  metrics would hash differently in the browser and signature verification would
  fail even for valid artifacts.
*/
function tokenizeJSON(str){
  var tokens=[],i=0;
  while(i<str.length){
    var c=str[i];
    if(c===' '||c==='\t'||c==='\n'||c==='\r'){i++;continue;}
    if(c==='"'){
      i++;var s='';
      while(i<str.length){
        var ch=str[i];
        if(ch==='\\'){i++;s+=str[i];i++;}
        else if(ch==='"'){i++;break;}
        else{s+=ch;i++;}
      }
      tokens.push({type:'string',value:s});
      continue;
    }
    if(c==='{'||c==='}'||c==='['||c===']'||c===':'||c===','){tokens.push({type:c});i++;continue;}
    if(str.substring(i,i+4)==='true'){tokens.push({type:'true',value:true});i+=4;continue;}
    if(str.substring(i,i+5)==='false'){tokens.push({type:'false',value:false});i+=5;continue;}
    if(str.substring(i,i+4)==='null'){tokens.push({type:'null',value:null});i+=4;continue;}
    if(c==='-'||(c>='0'&&c<='9')){
      var start=i;
      if(str[i]==='-')i++;
      while(i<str.length&&str[i]>='0'&&str[i]<='9')i++;
      if(str[i]==='.'){i++;while(i<str.length&&str[i]>='0'&&str[i]<='9')i++;}
      if(str[i]==='e'||str[i]==='E'){i++;if(str[i]==='+'||str[i]==='-')i++;while(i<str.length&&str[i]>='0'&&str[i]<='9')i++;}
      tokens.push({type:'number',raw:str.substring(start,i)});
      continue;
    }
    throw new Error('Unexpected character '+c+' at '+i);
  }
  return tokens;
}
function parseJSONPreserveNumbers(text){
  var tokens=tokenizeJSON(text),pos=0;
  function parseValue(){
    var tok=tokens[pos];
    if(tok.type==='number'){pos++;return{__num:tok.raw};}
    if(tok.type==='string'){pos++;return tok.value;}
    if(tok.type==='true'||tok.type==='false'||tok.type==='null'){pos++;return tok.value;}
    if(tok.type==='[')return parseArray();
    if(tok.type==='{')return parseObject();
    throw new Error('Unexpected token '+tok.type);
  }
  function parseArray(){
    pos++;
    var arr=[];
    if(tokens[pos]&&tokens[pos].type===']'){pos++;return arr;}
    while(true){
      arr.push(parseValue());
      var tok=tokens[pos++];
      if(tok.type===',')continue;
      if(tok.type===']')return arr;
      throw new Error('Expected , or ] got '+tok.type);
    }
  }
  function parseObject(){
    pos++;
    var obj={};
    if(tokens[pos]&&tokens[pos].type==='}'){pos++;return obj;}
    while(true){
      var keyTok=tokens[pos++];
      if(keyTok.type!=='string')throw new Error('Expected string key');
      var key=keyTok.value;
      if(tokens[pos++].type!==':')throw new Error('Expected :');
      obj[key]=parseValue();
      var tok=tokens[pos++];
      if(tok.type===',')continue;
      if(tok.type==='}')return obj;
      throw new Error('Expected , or } got '+tok.type);
    }
  }
  var val=parseValue();
  if(pos!==tokens.length)throw new Error('Unexpected trailing tokens');
  return val;
}
function sortedJSON(obj){
  if(obj && typeof obj==='object' && obj.__num!==undefined)return obj.__num;
  if(obj===null)return'null';
  if(typeof obj==='string')return JSON.stringify(obj);
  if(typeof obj==='number')return String(Number.isFinite(obj)?obj:'null');
  if(typeof obj==='boolean')return String(obj);
  if(Array.isArray(obj))return'['+obj.map(sortedJSON).join(',')+']';
  var keys=Object.keys(obj).sort(),p=[];
  for(var i=0;i<keys.length;i++){var k=keys[i];if(obj[k]!==undefined)p.push(JSON.stringify(k)+':'+sortedJSON(obj[k]));}
  return'{'+p.join(',')+'}';
}
function normalizeCreatedAt(manifest){
  if(manifest && typeof manifest==='object' && typeof manifest.created_at==='string'){
    var m=/^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2})(\.\d+)?(Z|[+-]\d{2}:\d{2})$/.exec(manifest.created_at);
    if(m)manifest.created_at=m[1]+'Z';
  }
  return manifest;
}
function computeManifestHash(rawManifestText){
  var manifest=parseJSONPreserveNumbers(rawManifestText);
  normalizeCreatedAt(manifest);
  delete manifest.signature;
  return sha256(new TextEncoder().encode(sortedJSON(manifest)));
}

async function canonicalStepHash(step){
  // Matches epi_core.serialize.get_canonical_hash(StepModel, format='json'):
  // sort keys, no whitespace, exclude source_type, and normalize only the
  // top-level datetime field (timestamp) the way Pydantic model_dump does.
  var fields=['index','timestamp','kind','content','trace_id','span_id','parent_span_id','prev_hash','governance'];
  var canonical={};
  for(var i=0;i<fields.length;i++){
    var k=fields[i];
    var v=step[k]!==undefined?step[k]:null;
    if(k==='timestamp'&&typeof v==='string'){
      var m=/^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2})(\.\d+)?(Z|[+-]\d{2}:\d{2})$/.exec(v);
      if(m)v=m[1]+'Z';
    }
    canonical[k]=v;
  }
  return sha256(new TextEncoder().encode(sortedJSON(canonical)));
}

function unwrapNum(v){
  return (v && typeof v==='object' && v.__num!==undefined)?Number(v.__num):v;
}
function auditStepSequence(steps){
  if(!steps||steps.length===0)return true;
  var indices=steps.map(function(s){var n=unwrapNum(s.index);return n!==undefined?n:0});
  for(var i=1;i<indices.length;i++){
    if(indices[i]!==indices[i-1]+1)return false;
  }
  var times=steps.map(function(s){
    var t_ns=unwrapNum((s.content||{}).timestamp_ns);
    return t_ns!==undefined?t_ns:(s.timestamp||'');
  });
  for(var i=1;i<times.length;i++){
    if(times[i]<times[i-1])return false;
  }
  return true;
}

function auditStepCompleteness(steps){
  var pendingToolCalls=[];
  var pendingLlmRequests=[];
  var pendingApprovals=[];
  for(var i=0;i<steps.length;i++){
    var s=steps[i];
    var kind=s.kind||'';
    var content=s.content||{};
    var idx=s.index!==undefined?s.index:0;
    var span_id=s.span_id;
    if(kind==='tool.call'){
      pendingToolCalls.push({idx:idx,call_id:content.call_id});
    }else if(kind==='tool.response'){
      var call_id=content.call_id;
      var matched=false;
      if(call_id!==undefined&&call_id!==null){
        for(var ti=pendingToolCalls.length-1;ti>=0;ti--){
          if(pendingToolCalls[ti].call_id===call_id){
            pendingToolCalls.splice(ti,1);
            matched=true;
            break;
          }
        }
      }
      if(!matched&&pendingToolCalls.length>0)pendingToolCalls.shift();
    }else if(kind==='llm.request'){
      pendingLlmRequests.push({idx:idx,span_id:span_id});
    }else if(kind==='llm.response'||kind==='llm.error'){
      var matched=false;
      if(span_id!==undefined&&span_id!==null){
        for(var li=pendingLlmRequests.length-1;li>=0;li--){
          if(pendingLlmRequests[li].span_id===span_id){
            pendingLlmRequests.splice(li,1);
            matched=true;
            break;
          }
        }
      }
      if(!matched&&pendingLlmRequests.length>0)pendingLlmRequests.shift();
    }else if(kind==='agent.approval.request'){
      pendingApprovals.push({idx:idx,action:content.action});
    }else if(kind==='agent.approval.response'){
      var action=content.action;
      var matched=false;
      if(action!==undefined&&action!==null){
        for(var ai=pendingApprovals.length-1;ai>=0;ai--){
          if(pendingApprovals[ai].action===action){
            pendingApprovals.splice(ai,1);
            matched=true;
            break;
          }
        }
      }
      if(!matched&&pendingApprovals.length>0)pendingApprovals.shift();
    }
  }
  return pendingToolCalls.length===0&&pendingLlmRequests.length===0&&pendingApprovals.length===0;
}

async function auditStepChain(steps){
  if(!steps||steps.length<2)return true;
  for(var i=1;i<steps.length;i++){
    var claimed=steps[i].prev_hash;
    if(claimed===null||claimed===undefined||claimed==='CHAIN_START')continue;
    var expected=await canonicalStepHash(steps[i-1]);
    if(claimed!==expected)return false;
  }
  return true;
}

function auditStepCount(steps,manifest){
  var total=unwrapNum(manifest.total_steps);
  if(total===undefined||total===null)return true;
  return steps.length===total;
}

async function auditSteps(steps,manifest){
  var seq=auditStepSequence(steps);
  var comp=auditStepCompleteness(steps);
  var chain=await auditStepChain(steps);
  var count=auditStepCount(steps,manifest);
  return{sequence_ok:seq,completeness_ok:comp,chain_ok:chain,step_count_ok:count};
}

async function processFile(f){
  var nm=f.name.toLowerCase();
  if(!nm.endsWith('.epi')&&!nm.endsWith('.zip')){showResult('fail','Invalid file type. Drop a .epi or .zip file.');return}
  showResult('warn','<em>Reading '+f.name+'...</em>');

  try{
    var buf=await f.arrayBuffer();
    var containerFmt=detectContainer(buf);
    if(!containerFmt){showResult('fail','<strong>Not a valid EPI file</strong><br>No EPI envelope or ZIP header detected.');return}

    var zipBuf=extractZIPPayload(buf,containerFmt);
    var zip;
    try{zip=await JSZip.loadAsync(zipBuf)}catch(e){showResult('fail','<strong>ZIP extraction failed</strong><br>'+e.message);return}

    var report={facts:{structure_ok:false,integrity_ok:false,signature_valid:null,has_signature:false,mismatches:{},chain_ok:true,sequence_ok:true,completeness_ok:true,transparency_ok:null},identity:{status:'UNKNOWN',name:null,detail:null,registry_verified:false,public_key_id:null},metadata:{spec_version:'?',workflow_id:'?',created_at:'?',files_checked:0,verifier_version:'browser',steps_count:null},trust_level:'NONE',trust_message:''};

    // Check 1: mimetype
    if(!zip.file('mimetype')){report.facts.structure_ok=false;showReport(report,'No mimetype file in archive');return}
    var mt=await zip.file('mimetype').async('string');
    if(mt.trim()!==EPI_MIMETYPE&&mt.trim()!=='application/vnd.epi'){report.facts.structure_ok=false;showReport(report,'Invalid mimetype: '+mt.trim());return}
    report.facts.structure_ok=true;

    // Check 2: manifest.json
    var mf=zip.file('manifest.json');
    if(!mf){showResult('fail','<strong>No manifest.json found</strong>');return}
    var rawManifest;
    try{rawManifest=await mf.async('string')}catch(e){showResult('fail','<strong>Could not read manifest.json</strong><br>'+e.message);return}
    var manifest;
    try{manifest=parseJSONPreserveNumbers(rawManifest)}catch(e){showResult('fail','<strong>manifest.json parse error</strong><br>'+e.message);return}
    report.metadata.spec_version=manifest.spec_version||'?';
    report.metadata.workflow_id=manifest.workflow_id||'?';
    report.metadata.created_at=manifest.created_at||'?';

    // Check 3: steps.jsonl — parse and count steps
    var steps=[],stepsFile=zip.file('steps.jsonl');
    if(stepsFile){
      var st=await stepsFile.async('string');
      var lines=st.split('\n');
      for(var si=0;si<lines.length;si++){
        var line=lines[si].trim();
        if(!line)continue;
        try{steps.push(parseJSONPreserveNumbers(line))}catch(e){}
      }
    }
    report.metadata.steps_count=steps.length;

    // Check 4: file_manifest integrity
    var fileManifest=manifest.file_manifest||{},mismatches={},checked=0;
    var manifestFiles=Object.keys(fileManifest);
    for(var j=0;j<manifestFiles.length;j++){
      var fname=manifestFiles[j],expected=fileManifest[fname],entry=zip.file(fname);
      if(!entry){mismatches[fname]='File missing from archive';continue}
      var data=await entry.async('uint8array'),actualHash=await sha256(data);
      checked++;
      if(actualHash!==expected){mismatches[fname]='Hash mismatch: expected '+expected.substring(0,12)+'... got '+actualHash.substring(0,12)+'...'}
    }
    // Check for extra unlisted files (not mimetype/manifest/viewer/review/artifacts)
    var excludePrefixes=['mimetype','manifest.json','viewer.html','VERIFY.txt','review.json','review_index.json','reviews/','artifacts/scitt/'];
    Object.keys(zip.files).forEach(function(nm){
      var excluded=false;
      for(var k=0;k<excludePrefixes.length;k++){if(nm.indexOf(excludePrefixes[k])===0){excluded=true;break}}
      if(!excluded&&!fileManifest[nm])mismatches[nm]='Extra file not in manifest';
    });
    report.metadata.files_checked=checked;
    report.facts.mismatches=mismatches;

    // Check 5: forensic audits on the step timeline
    var forensic=await auditSteps(steps,manifest);
    report.facts.sequence_ok=forensic.sequence_ok;
    report.facts.completeness_ok=forensic.completeness_ok;
    report.facts.chain_ok=forensic.chain_ok;
    report.facts.step_count_ok=forensic.step_count_ok;
    report.facts.integrity_ok=Object.keys(mismatches).length===0&&forensic.sequence_ok&&forensic.completeness_ok&&forensic.chain_ok&&forensic.step_count_ok;

    // Check 6: Signature + key-name binding + trust registry
    var registry=null;
    try{registry=await (await fetch('/.well-known/epi-trust-registry.json')).json();}catch(e){}
    report.facts.has_signature=!!manifest.signature;
    if(manifest.signature&&manifest.public_key){
      try{
        // Compute canonical hash matching Python's get_canonical_hash
        var manifestHash=await computeManifestHash(rawManifest);
        var sigResult=await verifyEd25519(manifest.signature,manifest.public_key,manifestHash);
        report.facts.signature_valid=sigResult.valid;
        report.identity.public_key_id=manifest.public_key.substring(0,16);
        var nameParts=manifest.signature.split(':',2);
        if(nameParts.length>=2)report.identity.name=nameParts[1];
        if(sigResult.valid===true){
          if(registry&&registry.trusted_keys&&registry.trusted_keys[manifest.public_key]){
            report.identity.status='KNOWN';
            report.identity.name=registry.trusted_keys[manifest.public_key];
            report.identity.detail='Verified via remote anchor: '+(registry.name||'EPI Labs Trust Registry');
            report.identity.registry_verified=true;
          }else{
            report.identity.status='UNKNOWN';
            report.identity.detail='Valid signature from unverified identity — verify signer independently';
          }
        }else{
          report.identity.status='UNKNOWN';
          report.identity.detail=sigResult.msg||'Signature could not be verified';
        }
      }catch(e){report.facts.signature_valid=null;report.identity.detail='WebCrypto not available: '+e.message}
    }else if(!manifest.signature){report.facts.signature_valid=null;report.identity.detail='Artifact is unsigned'}

    // Determine trust level (matching Python epi_core/trust.py)
    if(report.facts.integrity_ok&&report.facts.signature_valid===true&&report.identity.status==='KNOWN'){report.trust_level='HIGH';report.trust_message='Cryptographically verified — signer identity confirmed'}
    else if(report.facts.integrity_ok&&report.facts.signature_valid===true&&report.identity.status!=='KNOWN'){report.trust_level='LOW';report.trust_message='Valid signature from unverified identity — verify signer independently'}
    else if(report.facts.integrity_ok&&!report.facts.has_signature){report.trust_level='MEDIUM';report.trust_message='Unsigned — integrity intact, no signer identity'}
    else if(!report.facts.integrity_ok){report.trust_level='NONE';report.trust_message='Integrity compromised — do not trust'}
    else if(report.facts.signature_valid===false){report.trust_level='NONE';report.trust_message='Signature invalid — artifact may be tampered'}
    else{report.trust_level='NONE';report.trust_message='Verification inconclusive — do not trust'}

    showReport(report,'');
  }catch(e){showResult('fail','<strong>Verification error</strong><br>'+e.message)}
}

function showResult(type,msg){
  var colors={pass:['var(--verified-bg)','var(--verified)','var(--verified)'],warn:['var(--warn-bg)','var(--warn)','var(--warn)'],fail:['var(--tamper-bg)','var(--tamper)','var(--tamper)']};
  var c=colors[type]||colors.fail;
  dr.style.display='block';dr.style.background=c[0];dr.style.border='1px solid '+c[1];dr.style.color=c[2];dr.innerHTML=msg;
}

function showReport(report,errMsg){
  if(errMsg){showResult('fail',errMsg);return}
  var type=report.trust_level==='HIGH'?'pass':report.trust_level==='MEDIUM'?'warn':'fail';
  showResult(type,reportDOM(report));
  updateChecks(report);
}

function resetChecks(){
  ['chk1','chk2','chk3','chk4','chk5'].forEach(function(id){var el=document.getElementById(id);if(el){el.classList.remove('pass');el.innerHTML='<span class="check-dot"></span> '+el.innerHTML.replace(/^.*?\d{2}\s*·\s*/,'')}});
  if(dr)dr.style.display='none';
}

if(dz&&fi&&dr){
  dz.addEventListener('click',function(){fi.click()});
  dz.addEventListener('keydown',function(e){
    if(e.key==='Enter'||e.key===' '){e.preventDefault();fi.click()}
  });
  ['dragenter','dragover','dragleave','drop'].forEach(function(ev){dz.addEventListener(ev,function(e){e.preventDefault()});document.body.addEventListener(ev,function(e){e.preventDefault()})});
  ['dragenter','dragover'].forEach(function(ev){dz.addEventListener(ev,function(){dz.classList.add('drag-over')})});
  ['dragleave','drop'].forEach(function(ev){dz.addEventListener(ev,function(){dz.classList.remove('drag-over')})});
  dz.addEventListener('drop',function(e){if(e.dataTransfer.files[0])resetChecks();processFile(e.dataTransfer.files[0])});
  fi.addEventListener('change',function(){if(fi.files[0]){resetChecks();processFile(fi.files[0])}});
}

