import {withAccountOperation} from '../account-operation.mjs';
import {advertisingLedger,advertisingEnabled} from './service.mjs';
import {createAdvertisingAge} from './age.mjs';
import {AdvertisingError} from './reward-ledger.mjs';
import {validateDeviceSave} from '../../dist/js/offline/device-schema.mjs';
import {deviceAdvertisingContext} from '../../dist/js/advertising/device-context.mjs';
import {canRetryDaily,dexQuestionPool,selectRetryQuestions,selectRetryReward,nextDailyReset} from '../../dist/js/pixelpotes/daily-review.mjs';
import {act,normalizeState} from '../../dist/js/pixelpotes/model.mjs';
import {activeGold} from '../billing.mjs';
import ja from '../../dist/assets/lexicon/ja.json' with {type:'json'};

const fail=(message,status=409)=>{throw Object.assign(new AdvertisingError('ad_device_unavailable',status),{message});};
const operation=value=>typeof value==='string'&&/^[a-zA-Z0-9-]{16,100}$/.test(value);
function dayInZone(at,timeZone='UTC'){
 try{if(typeof timeZone!=='string'||timeZone.length>80)throw Error();const p=new Intl.DateTimeFormat('en-US',{timeZone,year:'numeric',month:'2-digit',day:'2-digit'}).formatToParts(at);return ['year','month','day'].map(k=>p.find(v=>v.type===k).value).join('-');}
 catch{fail('Fuseau horaire invalide.',400);}
}
export const deviceRewardsEnabled=env=>advertisingEnabled(env)&&env.PIXELINGO_AD_DEVICE_REWARDS_ENABLED==='true';
// Grants affect SOLO daily attempts only. Never import the backup's money, XP,
// rights or inventory into account/competitive tables. No test bypass here.
export function createDeviceAdvertisingRewards(env,{now=Date.now,random=Math.random,ledger:providedLedger}={}){
 const db=env.DB,age=createAdvertisingAge({db,now});
 function requireUser(user){if(!deviceRewardsEnabled(env))fail('Publicité indisponible pour le moment.',404);if(!user?.member)fail('Connecte le compte de cette partie.',401);if(!db)fail('Service indisponible.',503);}
 async function context(user,profileId,timeZone='UTC'){
  requireUser(user);
  if(typeof profileId!=='string'||!/^device-[a-f0-9-]{36}$/.test(profileId))fail('Partie inconnue.',400);
  if(await db.prepare('SELECT user_id FROM account_deletions WHERE user_id=?').bind(user.id).first())fail('Le compte est en cours de fermeture.');
  const row=await db.prepare('SELECT payload FROM device_saves WHERE user_id=? AND profile_id=?').bind(user.id,profileId).first();
  if(!row)fail('Rattache cette partie à ton compte dans Mon compte.');
  const save=validateDeviceSave(JSON.parse(row.payload)),c=await deviceAdvertisingContext(save);
  // Match the game's local day using SERVER time. A separate UTC account quota
  // prevents changing time zones to obtain another grant in the same quota day.
  const day=dayInZone(now(),timeZone),quotaDay=new Date(now()).toISOString().slice(0,10);
  if(save.flow?.status!=='completed'||!save.pot.created||!canRetryDaily(c.daily,day))fail('Cette tentative ne peut plus être relancée.');
  if(await activeGold(db,env.PIXELINGO_BILLING_MODE||'test',user.id,now()))fail('Les relances Pixel Star terminent leur configuration.');
  if(await db.prepare('SELECT intent_id FROM advertising_device_grants WHERE user_id=? AND (day=? OR quota_day=?) AND language=?').bind(user.id,day,quotaDay,'ja').first())fail('La relance publicitaire de ce jour a déjà été utilisée.');
  const entries=[...Object.values(save.pot.dexRewards||{}),...Object.values(save.promenadeGift||{})].filter(e=>e?.language==='ja');
  const seen=new Set(entries.map(e=>e.id));
  const questions=selectRetryQuestions(dexQuestionPool({entries,catalogue:ja},'ja'),c.daily,random);
  const rewardCandidate=selectRetryReward(ja.entries.filter(e=>!seen.has(e.id)).map(e=>({id:e.id,language:'ja',source:e.displayWord,meanings:e.meanings,readings:e.readings})),c.daily,save.pot,random);
  return {...c,ownerId:user.id,quotaDay,dayEndsAt:Math.min(nextDailyReset(now(),timeZone),Date.parse(quotaDay+'T00:00:00Z')+86400000),questions,rewardCandidate,save};
 }
 const ledgerFor=profileId=>providedLedger??advertisingLedger(env,{now,authorizeContext:user=>context(user,profileId)});
 async function options(user,profileId,timeZone){
  requireUser(user);const declared=await age.read(user);
  if(declared.band==='under-16')return {ownerId:user.id,profileId,ad:false,starRightId:null,unavailable:'Publicité indisponible.'};
  try{const c=await context(user,profileId,timeZone);return {ownerId:user.id,profileId,day:c.day,language:c.language,attempt:c.attempt,contextKey:c.contextKey,ad:true,starRightId:null,requiresAgeDeclaration:!declared.band};}
  catch(e){if(e.status===409)return {ownerId:user.id,profileId,ad:false,starRightId:null,unavailable:e.message};throw e;}
 }
 async function prepare(user,profileId,body){
  requireUser(user);const c=await context(user,profileId,body?.timeZone);
  if(body?.contextKey!==c.contextKey)fail('La partie a changé. Sauvegarde-la avant de réessayer.');
  // Recheck the expected context INSIDE the ledger's account lease.
  const ledger=providedLedger??advertisingLedger(env,{now,authorizeContext:async u=>{const current=await context(u,profileId,body.timeZone);if(current.contextKey!==body.contextKey)fail('La partie a changé.');return current;}});
  return {...await ledger.prepare(user,body.platform),profileId,contextKey:c.contextKey,ownerId:user.id};
 }
 async function status(user,profileId,id,timeZone){
  requireUser(user);await age.read(user);
  const grant=await db.prepare('SELECT result FROM advertising_device_grants WHERE user_id=? AND profile_id=? AND intent_id=?').bind(user.id,profileId,id).first();
  if(grant)return {id,completed:false,consumed:true,grant:JSON.parse(grant.result)};
  const c=await context(user,profileId,timeZone),row=await db.prepare('SELECT context_key FROM advertising_intents WHERE id=? AND user_id=?').bind(id,user.id).first();
  if(!row||row.context_key!==c.contextKey)fail('Cette confirmation appartient à une autre tentative.');
  return ledgerFor(profileId).status(user,id);
 }
 async function consume(user,profileId,body){
  requireUser(user);if(!operation(body?.operationId)||typeof body?.proof!=='string')fail('Relance invalide.',400);
  return withAccountOperation(db,user.id,async lease=>{
   await age.requireEligible(user);
   const prior=await db.prepare('SELECT result FROM advertising_device_grants WHERE user_id=? AND profile_id=? AND intent_id=?').bind(user.id,profileId,body.proof).first();
   if(prior){const result=JSON.parse(prior.result);if(body.contextKey!==result.contextKey)fail('La tentative a changé.');return {...result,replayed:true};}
   const c=await context(user,profileId,body.timeZone);if(body.contextKey!==c.contextKey)fail('La tentative a changé.');
   const proof=await ledgerFor(profileId).consumption(user,body.proof,c,body.operationId);
   const action={type:'daily-retry',day:c.day,language:c.language,attempt:c.attempt,receiptId:proof.receiptId};
   const applied=act(normalizeState(c.save.pot,now()),action,now(),{dailyDay:c.day,dailyRetryGrant:{...c,...action,source:'ad'}});
   if(!applied.ok)fail(applied.message);
   // Return only the generated daily attempt; the device applies it without
   // replacing its inventory, wallet or unrelated progress.
   const result={ok:true,testOnly:false,authority:'solo-daily-retry',accountId:user.id,profileId,contextKey:c.contextKey,intentId:body.proof,receiptId:proof.receiptId,day:c.day,language:c.language,attempt:c.attempt,daily:applied.state.dailyReview};
   await lease.finalize([...proof.writes,db.prepare('INSERT INTO advertising_device_grants(user_id,profile_id,day,quota_day,language,intent_id,operation_id,result,created_at) VALUES(?,?,?,?,?,?,?,?,?)').bind(user.id,profileId,c.day,c.quotaDay,c.language,body.proof,body.operationId,JSON.stringify(result),now())]);
   return result;
  },now);
 }
 return {options,prepare,status,consume,context};
}
