function createWeekPlan({fetchJson,localDate,timeZone,storage,getLogin}={}) {
  let week={revision:0,timezone:null,days:[]};
  let flushing=null;
  let lastConflict=null;
  const storagePrefix='stackchain.week-sync.v1.';
  const cloneDay=day=>({
    plan_date:day.plan_date,ids:[...(day.ids||[])],capacity_minutes:day.capacity_minutes??null,
    estimates:{...(day.estimates||{})},
  });
  const state=()=>({revision:week.revision,timezone:week.timezone,days:week.days.map(cloneDay),
    ...(week.sync_pending?{base_revision:week.base_revision,sync_pending:true}:{})});
  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?.days)?{
        revision:value.base_revision,base_revision:value.base_revision,timezone:value.timezone||null,
        days:value.days.map(cloneDay).sort((left,right)=>left.plan_date.localeCompare(right.plan_date)),sync_pending:true,
        ...(Array.isArray(value.base_days)?{base_days:value.base_days.map(cloneDay)
          .sort((left,right)=>left.plan_date.localeCompare(right.plan_date))}:{}),
        ...(value.resolutions&&typeof value.resolutions==='object'?{resolutions:{...value.resolutions}}:{}),
      }:false;
    } catch(_error) { return false; }
  }
  function adopt(value) {
    if(!Number.isInteger(value?.revision)||!Array.isArray(value?.days)) return false;
    week={revision:value.revision,timezone:value.timezone||null,days:value.days.map(cloneDay)
      .sort((left,right)=>left.plan_date.localeCompare(right.plan_date))};
    lastConflict=null;
    return state();
  }
  function addDays(value, amount) {
    const [year,month,day]=value.split('-').map(Number);
    return new Date(Date.UTC(year,month-1,day+amount)).toISOString().slice(0,10);
  }
  function dates() {
    return Array.from({length:7},(_,index)=>{
      const date=addDays(localDate(),index+1);
      const parsed=new Date(date+'T12:00:00Z');
      return {date,label:new Intl.DateTimeFormat('en',{weekday:'short',month:'short',day:'numeric',timeZone:'UTC'}).format(parsed)};
    });
  }
  function day(planDate) {
    const found=week.days.find(item=>item.plan_date===planDate);
    return found?cloneDay(found):{plan_date:planDate,ids:[],capacity_minutes:null,estimates:{}};
  }
  async function load() {
    const queued=pending();
    if(queued){week=queued;return state();}
    return adopt(await fetchJson('api/v1/week'));
  }
  function stageDay(planDate,value) {
    const key=storageKey();
    if(!key||!storage) return false;
    const queued={base_revision:Number.isInteger(week.base_revision)?week.base_revision:week.revision,
      timezone:timeZone(),days:week.days.filter(item=>item.plan_date!==planDate).concat([{
        plan_date:planDate,ids:[...(value.ids||[])],capacity_minutes:value.capacity_minutes??null,
        estimates:{...(value.estimates||{})},
      }]).sort((left,right)=>left.plan_date.localeCompare(right.plan_date)),
      base_days:(week.base_days||week.days).map(cloneDay)};
    try { storage.setItem(key,JSON.stringify(queued)); }
    catch(_error) { return false; }
    lastConflict=null;
    week={revision:queued.base_revision,...queued,sync_pending:true};
    return state();
  }
  function deliveryBody(value) {
    return {base_revision:value.base_revision,timezone:value.timezone,days:value.days.map(cloneDay)};
  }
  function reconcile(local,remote) {
    if(!Array.isArray(local.base_days)) return null;
    const byDate=(days,date)=>days.find(day=>day.plan_date===date)||
      {plan_date:date,ids:[],capacity_minutes:null,estimates:{}};
    const same=(left,right)=>JSON.stringify(cloneDay(left))===JSON.stringify(cloneDay(right));
    const dates=[...new Set([...local.base_days,...local.days,...remote.days].map(day=>day.plan_date))].sort();
    const conflicts=[],days=dates.map(date=>{
      const base=byDate(local.base_days,date),phone=byDate(local.days,date),account=byDate(remote.days,date);
      const phoneChanged=!same(phone,base),accountChanged=!same(account,base);
      if(phoneChanged&&accountChanged&&!same(phone,account)){
        conflicts.push({plan_date:date,local:cloneDay(phone),remote:cloneDay(account)});return cloneDay(phone);
      }
      return cloneDay(phoneChanged?phone:account);
    });
    return {days,conflicts};
  }
  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/week',{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/week');
          const local=pending();
          const merged=local&&reconcile(local,remote);
          if(merged&&!merged.conflicts.length){
            const saved=await fetchJson('api/v1/week',{method:'PUT',headers:{'Content-Type':'application/json'},
              body:JSON.stringify({base_revision:remote.revision,timezone:local.timezone,days:merged.days})});
            const current=pending();
            if(current&&JSON.stringify(deliveryBody(current))===JSON.stringify(body)) storage.removeItem(key);
            if(!pending()) adopt(saved);
            return saved;
          }
          lastConflict={key,local,remote:{revision:remote.revision,timezone:remote.timezone||null,
            days:remote.days.map(cloneDay)},merged,choices:{...(local.resolutions||{})}};
        }
        throw error;
      }).finally(()=>{flushing=null;});
    return flushing;
  }
  async function saveDay(planDate, value) {
    const local={revision:week.revision,timezone:timeZone(),days:week.days
      .filter(item=>item.plan_date!==planDate).concat([{
        plan_date:planDate,ids:[...(value.ids||[])],capacity_minutes:value.capacity_minutes??null,
        estimates:{...(value.estimates||{})},
      }]).sort((left,right)=>left.plan_date.localeCompare(right.plan_date))};
    const body={base_revision:local.revision,timezone:local.timezone,days:local.days};
    try {
      return adopt(await fetchJson('api/v1/week',{method:'PUT',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)}));
    } catch(error) {
      if(error?.status===409) {
        const remote=await fetchJson('api/v1/week');
        lastConflict={local,remote:{revision:remote.revision,timezone:remote.timezone,days:remote.days.map(cloneDay)}};
      }
      throw error;
    }
  }
  function conflict() {
    const value=lastConflict&&(!lastConflict.key||lastConflict.key===storageKey())?{
      local:{...lastConflict.local,days:lastConflict.local.days.map(cloneDay)},
      remote:{...lastConflict.remote,days:lastConflict.remote.days.map(cloneDay)},
    }:null;
    if(value&&lastConflict.merged) value.conflicts=lastConflict.merged.conflicts.map(item=>({
      plan_date:item.plan_date,local:cloneDay(item.local),remote:cloneDay(item.remote),
      choice:lastConflict.choices[item.plan_date]||null,
    }));
    return value;
  }
  function chooseDay(planDate,source) {
    if(!lastConflict?.merged||!['phone','account'].includes(source)||
      !lastConflict.merged.conflicts.some(item=>item.plan_date===planDate)) return false;
    const key=storageKey(),queued=pending();
    if(key&&queued){
      try {
        const raw=JSON.parse(storage.getItem(key));
        raw.resolutions={...(raw.resolutions||{}),[planDate]:source};
        storage.setItem(key,JSON.stringify(raw));
      } catch(_error) { return false; }
    }
    lastConflict.choices[planDate]=source;
    return conflict();
  }
  async function saveMerged() {
    if(!lastConflict?.merged||lastConflict.key!==storageKey()) return false;
    if(lastConflict.merged.conflicts.some(item=>!lastConflict.choices[item.plan_date])) return false;
    const choices=lastConflict.choices;
    const days=lastConflict.merged.days.map(day=>{
      const item=lastConflict.merged.conflicts.find(value=>value.plan_date===day.plan_date);
      return item?cloneDay(choices[day.plan_date]==='account'?item.remote:item.local):cloneDay(day);
    });
    const local=lastConflict.local,body={base_revision:lastConflict.remote.revision,timezone:local.timezone,days};
    try {
      const saved=await fetchJson('api/v1/week',{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/week'),merged=reconcile(local,remote);
        lastConflict={key:storageKey(),local,remote:{revision:remote.revision,timezone:remote.timezone||null,
          days:remote.days.map(cloneDay)},merged,choices:{...(local.resolutions||{})}};
      }
      throw error;
    }
  }
  async function keepLocal() {
    if(!lastConflict||lastConflict.key!==storageKey()) return false;
    const conflictKey=lastConflict.key;
    const local={...lastConflict.local,days:lastConflict.local.days.map(cloneDay)};
    const body=deliveryBody({...local,base_revision:lastConflict.remote.revision});
    try {
      const saved=await fetchJson('api/v1/week',{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/week');
        lastConflict={key:conflictKey,local,remote:{revision:remote.revision,timezone:remote.timezone||null,
          days:remote.days.map(cloneDay)}};
      }
      throw error;
    }
  }
  function useRemote() {
    if(!lastConflict||lastConflict.key!==storageKey()) return false;
    const remote=lastConflict.remote;
    storage.removeItem(storageKey());
    const adopted=adopt(remote);
    lastConflict=null;
    return adopted;
  }
  async function promote(todayRevision) {
    if(pending()||conflict()) return false;
    const due=week.days.find(item=>item.plan_date<=localDate()&&item.ids.length);
    if(!due) return false;
    return fetchJson('api/v1/week/promote',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({
      promotion_id:`week-${due.plan_date}-r${week.revision}`,week_revision:week.revision,
      plan_date:due.plan_date,today_revision:todayRevision,
    })});
  }
  function summary() {
    const planned=week.days.filter(item=>item.ids.length);
    const items=planned.reduce((total,item)=>total+item.ids.length,0);
    const label=planned.length?`${items} item${items===1?'':'s'} across ${planned.length} day${planned.length===1?'':'s'}`:'Nothing planned';
    return label+(pending()?' · sync pending':'');
  }
  return {adopt,state,dates,day,load,saveDay,stageDay,pending,flush,conflict,chooseDay,saveMerged,
    keepLocal,useRemote,promote,summary};
}
function createWeekPlanWorkflow({controller,qs,getLogin,openPlanner,escapeHtml,escapeAttribute,
  todayWork,refresh,warm}={}) {
  let selectedDate=null;
  let blockedReviewOpen=false;
  function conflictDetail(day) {
    const estimates=day.estimates||{},ids=day.ids||[];
    const minutes=ids.reduce((total,id)=>total+(Number(estimates[id])||0),0),capacity=Number(day.capacity_minutes)||0;
    return ids.length+' item'+(ids.length===1?'':'s')+(minutes&&capacity?' · '+minutes+' of '+capacity+' min':'');
  }
  function conflictMarkup(value) {
    const days=value?.days||[];
    return days.length?days.map(day=>'<p class="week-conflict-day"><strong>'+escapeHtml(day.plan_date)+'</strong>'+escapeHtml(conflictDetail(day))+'</p>').join(''):
      '<p class="muted">Nothing planned.</p>';
  }
  function renderConflict(conflict) {
    qs('#week-conflict-phone-plan').innerHTML=conflictMarkup(conflict.local);
    qs('#week-conflict-server-plan').innerHTML=conflictMarkup(conflict.remote);
    const perDay=Array.isArray(conflict.conflicts),days=qs('#week-conflict-days'),save=qs('#save-merged-week');
    qs('#week-conflict-legacy-plans').hidden=perDay;qs('#week-conflict-legacy-actions').hidden=perDay;
    days.hidden=!perDay;save.hidden=!perDay;
    if(perDay){
      days.innerHTML=conflict.conflicts.map(item=>{
        const date=escapeAttribute(item.plan_date),name='week-conflict-'+date;
        const choice=source=>'<label class="week-conflict-choice"><input type="radio" name="'+name+
          '" data-week-conflict-choice="'+source+'" data-week-conflict-date="'+date+'"'+(item.choice===source?' checked':'')+
          '><span><strong>'+(source==='phone'?'This phone':'Saved account')+'</strong><small>'+escapeHtml(conflictDetail(source==='phone'?item.local:item.remote))+'</small></span></label>';
        return '<section class="week-conflict-date"><h4>'+escapeHtml(item.plan_date)+'</h4>'+choice('phone')+choice('account')+'</section>';
      }).join('');
      save.disabled=conflict.conflicts.some(item=>!item.choice);
      days.querySelectorAll('[data-week-conflict-choice]').forEach(input=>input.addEventListener('change',event=>{
        controller.chooseDay(event.currentTarget.dataset.weekConflictDate,event.currentTarget.dataset.weekConflictChoice);
        save.disabled=controller.conflict().conflicts.some(item=>!item.choice);
      }));
    }
    return perDay?days.querySelector('[data-week-conflict-choice]'):qs('#keep-phone-week');
  }
  function renderDates() {
    const root=qs('#week-plan-dates');
    root.hidden=!selectedDate;
    if(!selectedDate)return;
    root.innerHTML=controller.dates().map(item=>'<button type="button" data-week-plan-date="'+
      escapeAttribute(item.date)+'"'+(item.date===selectedDate?' aria-current="date"':'')+'><strong>'+escapeHtml(item.label)+
      '</strong><small>'+escapeHtml(controller.day(item.date).ids.length+' planned')+'</small></button>').join('');
    root.querySelectorAll('[data-week-plan-date]').forEach(button=>button.addEventListener('click',()=>{
      selectedDate=button.dataset.weekPlanDate;renderDates();openPlanner(null,false);
    }));
  }
  async function open(trigger) {
    trigger.disabled=true;selectedDate=controller.dates()[0].date;renderDates();openPlanner(trigger);
    qs('#mobile-week-summary').textContent='Loading Week Ahead…';
    try{await controller.load();qs('#mobile-week-summary').textContent=controller.summary();openPlanner(null,false);return true;}
    catch(error){qs('#mobile-week-summary').textContent='Unavailable · tap to retry';qs('#my-work-action-status').textContent=(error.message||'Week Ahead is unavailable.')+' Retry when connected.';return false;}
    finally{trigger.disabled=false;}
  }
  function save(plan){
    const date=selectedDate,normalized=Array.isArray(plan)?{ids:plan,capacity_minutes:null,estimates:{}}:plan;
    const staged=controller.stageDay(date,normalized);
    if(!staged){qs('#my-work-action-status').textContent='Week Ahead could not be saved on this phone. Free browser storage and retry.';return false;}
    qs('#mobile-week-summary').textContent=controller.summary();
    qs('#my-work-action-status').textContent='Week Ahead saved on this phone · sync pending.';
    controller.flush().then(()=>{qs('#mobile-week-summary').textContent=controller.summary();qs('#my-work-action-status').textContent=(normalized.ids.length?'Week Ahead saved for ':'Week Ahead cleared for ')+date+'.';})
      .catch(error=>{qs('#mobile-week-summary').textContent=controller.conflict()?'Conflict · review required':controller.summary();qs('#my-work-action-status').textContent=controller.conflict()?'Another device changed Week Ahead. Both versions are preserved; open Week Ahead to choose one.':(error.message||'Week Ahead sync is unavailable.')+' Saved on this phone · sync pending.';});
    return true;
  }
  async function promote(plan){
    try{await controller.load();const promoted=await controller.promote(plan.revision);if(!promoted)return false;blockedReviewOpen=false;todayWork.replace(promoted.ids);todayWork.replacePlanning({capacity_minutes:promoted.capacity_minutes??null,estimates:promoted.estimates||{}});refresh();warm();qs('#mobile-week-summary').textContent=controller.summary();qs('#my-work-action-status').textContent='Your saved Week Ahead plan is now Today.';return true;}
    catch(error){
      if(error?.code==='week_today_in_progress'){
        qs('#my-work-action-status').textContent='Review unfinished Today before starting the saved Week Ahead day.';
        if(!blockedReviewOpen){blockedReviewOpen=true;openPlanner(null,false);}
        return false;
      }
      blockedReviewOpen=false;
      qs('#my-work-action-status').textContent=(error.message||'Week Ahead needs review before promotion.')+' Open Week Ahead to review.';return false;
    }
  }
  return {open,save,promote,renderDates,renderConflict,active:()=>Boolean(selectedDate),selectedDate:()=>selectedDate,
    day:()=>selectedDate?controller.day(selectedDate):null,
    copy:()=>selectedDate?{title:'Plan Week Ahead',heading:selectedDate+', in order',available:'Available this day',build:'Build this day'}:null,
    clear(){selectedDate=null;renderDates();}};
}
if(typeof module!=='undefined'&&module.exports){
  module.exports=createWeekPlan;
  module.exports.Workflow=createWeekPlanWorkflow;
}
