function createTomorrowPlan({fetchJson,localDate,timeZone,storage,getLogin}={}) {
  let plan={revision:0,ids:[],capacity_minutes:null,estimates:{}};
  let flushing=null;
  let lastConflict=null;
  const storagePrefix='stackchain.tomorrow-sync.v1.';
  const state=()=>({...plan,ids:[...plan.ids],estimates:{...plan.estimates}});
  function storageKey() {
    const login=String(getLogin?.()||'').trim().toLowerCase();
    return login?storagePrefix+encodeURIComponent(login):'';
  }
  function pending() {
    const key=storageKey();
    if(!key||!storage) return false;
    try {
      const value=JSON.parse(storage.getItem(key)||'null');
      return Number.isInteger(value?.base_revision)&&Array.isArray(value?.ids)?
        {...value,ids:[...value.ids],estimates:{...(value.estimates||{})},sync_pending:true}:false;
    } catch (_error) { return false; }
  }
  function adopt(value) {
    if (!Number.isInteger(value?.revision)||!Array.isArray(value?.ids)) return false;
    plan={revision:value.revision,ids:[...value.ids],capacity_minutes:value.capacity_minutes??null,
      estimates:{...(value.estimates||{})},...(value.plan_date?{plan_date:value.plan_date,timezone:value.timezone||null}:{})};
    return state();
  }
  function nextLocalDate() {
    const [y,m,d]=localDate().split('-').map(Number);
    return new Date(Date.UTC(y,m-1,d+1)).toISOString().slice(0,10);
  }
  async function load(){
    const queued=pending();
    return queued?(plan={...queued},state()):adopt(await fetchJson('api/v1/tomorrow'));
  }
  function stage(value) {
    const key=storageKey();
    if(!key||!storage) return false;
    const queued={base_revision:plan.revision,revision:plan.revision,ids:[...(value.ids||[])],
      capacity_minutes:value.capacity_minutes??null,estimates:{...(value.estimates||{})},
      plan_date:nextLocalDate(),timezone:timeZone(),sync_pending:true};
    try { storage.setItem(key,JSON.stringify(queued)); }
    catch (_error) { return false; }
    lastConflict=null;
    plan=queued;
    return state();
  }
  function deliveryBody(value) {
    return {base_revision:value.base_revision,ids:[...value.ids],
      capacity_minutes:value.capacity_minutes??null,estimates:{...(value.estimates||{})},
      plan_date:value.plan_date,timezone:value.timezone};
  }
  function flush() {
    if(flushing) return flushing;
    const queued=pending();
    const key=storageKey();
    if(!queued||!key) return Promise.resolve(false);
    const body=deliveryBody(queued);
    flushing=fetchJson('api/v1/tomorrow',{method:'PUT',headers:{'Content-Type':'application/json'},
      body:JSON.stringify(body)}).then(saved=>{
      const current=pending();
      if(current&&JSON.stringify(deliveryBody(current))===JSON.stringify(body)) storage.removeItem(key);
      if(!pending()) adopt(saved);
      return saved;
    }).catch(async error=>{
      if(error?.status===409) {
        const remote=await fetchJson('api/v1/tomorrow');
        lastConflict={key,local:pending(),remote:{...remote,ids:[...remote.ids],estimates:{...(remote.estimates||{})}}};
      }
      throw error;
    }).finally(()=>{flushing=null;});
    return flushing;
  }
  async function keepLocal() {
    if(!lastConflict||lastConflict.key!==storageKey()) return false;
    const conflictKey=lastConflict.key;
    const local={...lastConflict.local,ids:[...lastConflict.local.ids],estimates:{...lastConflict.local.estimates}};
    const body=deliveryBody({...local,base_revision:lastConflict.remote.revision});
    try {
      const saved=await fetchJson('api/v1/tomorrow',{method:'PUT',headers:{'Content-Type':'application/json'},
        body:JSON.stringify(body)});
      const current=pending();
      if(current&&JSON.stringify(deliveryBody(current))===JSON.stringify(deliveryBody(local))) storage.removeItem(storageKey());
      if(!pending()) adopt(saved);
      lastConflict=null;
      return saved;
    } catch(error) {
      if(error?.status===409) {
        const remote=await fetchJson('api/v1/tomorrow');
        lastConflict={key:conflictKey,local,remote:{...remote,ids:[...remote.ids],estimates:{...(remote.estimates||{})}}};
      }
      throw error;
    }
  }
  function useRemote() {
    if(!lastConflict||lastConflict.key!==storageKey()) return false;
    const remote=lastConflict.remote;
    const key=storageKey();
    if(key&&storage) storage.removeItem(key);
    const adopted=adopt(remote);
    lastConflict=null;
    return adopted;
  }
  async function save(value) {
    const body={base_revision:plan.revision,ids:[...(value.ids||[])],
      capacity_minutes:value.capacity_minutes??null,estimates:{...(value.estimates||{})},
      plan_date:nextLocalDate(),timezone:timeZone()};
    return adopt(await fetchJson('api/v1/tomorrow',{method:'PUT',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)}));
  }
  async function promote(today_revision) {
    if (pending()||!plan.plan_date||plan.plan_date>localDate()||!plan.ids.length) return false;
    const promotion_id=`tomorrow-${plan.plan_date}-r${plan.revision}`;
    return fetchJson('api/v1/tomorrow/promote',{method:'POST',headers:{'Content-Type':'application/json'},
      body:JSON.stringify({promotion_id,tomorrow_revision:plan.revision,today_revision})});
  }
  function millisecondsUntilNextDay() {
    const now=Date.now();
    const zone=timeZone();
    let formatter;
    try {
      formatter=new Intl.DateTimeFormat('en-CA',{timeZone:zone,year:'numeric',month:'2-digit',day:'2-digit',
        hour:'2-digit',minute:'2-digit',second:'2-digit',hourCycle:'h23'});
    } catch (_error) {
      const next=new Date(now);
      next.setHours(24,0,0,250);
      return Math.max(1,next.getTime()-now);
    }
    const zoned=value=>Object.fromEntries(formatter.formatToParts(new Date(value))
      .filter(part=>part.type!=='literal').map(part=>[part.type,Number(part.value)]));
    const current=zoned(now);
    const targetWall=Date.UTC(current.year,current.month-1,current.day+1);
    let candidate=targetWall;
    for(let attempt=0;attempt<2;attempt+=1){
      const parts=zoned(candidate);
      const offset=Date.UTC(parts.year,parts.month-1,parts.day,parts.hour,parts.minute,parts.second)-candidate;
      candidate=targetWall-offset;
    }
    return Math.max(1,candidate-now+250);
  }
  function summary(value=plan) {
    const ids=Array.isArray(value?.ids)?value.ids:[];
    const suffix=value?.sync_pending?' · sync pending':'';
    if(!ids.length) return 'Nothing planned'+suffix;
    const label=`${ids.length} planned`;
    const estimates=value.estimates||{};
    const estimated=ids.reduce((total,id)=>total+(Number(estimates[id])||0),0);
    const capacity=Number(value.capacity_minutes)||0;
    return (estimated&&capacity?`${label} · ${estimated} of ${capacity} min`:label)+suffix;
  }
  function startLifecycle({windowObject,documentObject,check,setTimer=setTimeout,clearTimer=clearTimeout,
    nextDelay=millisecondsUntilNextDay}={}) {
    let flight=null;
    let timer=null;
    const schedule=()=>{
      if(timer!==null) clearTimer(timer);
      timer=setTimer(run,nextDelay());
    };
    function run(){
      if(!flight) flight=Promise.resolve().then(check).finally(()=>{flight=null;schedule();});
      return flight;
    }
    windowObject?.addEventListener?.('online',run);
    documentObject?.addEventListener?.('visibilitychange',()=>documentObject.hidden?false:run());
    run();
    return {run,stop(){if(timer!==null)clearTimer(timer);timer=null;}};
  }
  function conflict() {
    return lastConflict?.key===storageKey()?{local:{...lastConflict.local,ids:[...lastConflict.local.ids],estimates:{...lastConflict.local.estimates}},
      remote:{...lastConflict.remote,ids:[...lastConflict.remote.ids],estimates:{...lastConflict.remote.estimates}}}:null;
  }
  return {adopt,load,save,stage,pending,flush,conflict,keepLocal,useRemote,promote,state,summary,nextLocalDate,startLifecycle};
}
if(typeof module!=='undefined'&&module.exports)module.exports=createTomorrowPlan;
