o
    ڒiEF                     @   sl   d Z ddlZddlZddlZddlZddlmZmZ ddlmZ ddl	m
Z
mZ eeZG dd dZdS )u|   
Dashboard data manager — reads and writes dashboard-data.json
with file locking and atomic writes to prevent corruption.
    N)datetimedate)Path)FileLockTimeoutc                   @   s  e Zd ZdefddZdefddZdefdd	Zd
d Zde	defddZ
de	defddZdededefddZ		dMdededededededefddZdNdededefd d!ZdOd"eded#ed$edef
d%d&Zd'edefd(d)Z	dPd*ed+ed,ed-ededefd.d/Zd0edefd1d2Zd3ed4edefd5d6Zd3ed7edefd8d9ZdNd:e	defd;d<Zdefd=d>Zdefd?d@ZdefdAdBZdQdDedefdEdFZdefdGdHZdefdIdJZdefdKdLZdS )RDashboardManager	file_pathc                 C   s.   t || _t| jd | _t| jdd| _d S )Nz.lock
   )timeout)r   r   str	lock_pathr   lock)selfr    r   dashboard_manager.py__init__   s   
zDashboardManager.__init__returnc              	   C   st   | j - t| jddd}t|W  d   W  d   S 1 s#w   Y  W d   dS 1 s3w   Y  dS )z(Read and return the full dashboard data.rutf-8encodingN)r   openr   jsonload)r   fr   r   r   read   s   "zDashboardManager.readdatac                 C   s   | j j}tjt|dd\}}z2tj|ddd}tj||ddd W d	   n1 s,w   Y  t	|t| j  t
d
 W d	S  tyT   tj|rSt|  w )z.Atomic write: write to temp file, then rename.z	.json.tmp)dirsuffixwr   r      F)indentensure_asciiNz#Dashboard data written successfully)r   parenttempfilemkstempr   osfdopenr   dumpreplaceloggerinfo	Exceptionpathexistsunlink)r   r   dir_pathfdtmp_pathr   r   r   r   _write!   s   
zDashboardManager._writec              	   C   s~   | j 2 t| jddd}t|}W d   n1 sw   Y  ||}| | ||fW  d   S 1 s8w   Y  dS )zDRead data, apply updater function, write back. Returns updated data.r   r   r   N)r   r   r   r   r   r3   )r   updaterr   r   resultr   r   r   _read_and_write/   s   
$z DashboardManager._read_and_writevaluec                        fdd}|  |\}}|S )zHUpdate weight with new value, rotate series, recalculate derived fields.c                    sl  | d }|d }t  | d|d<  |d< |dg }|  t|dkr,|dd  }||d< t|dkr<|d	d  n|}t t|t| d|d
< |dd}|dd}tddd}tt | jd d}|  }	t |	| d}
|
|d<  | }|
dkrt ||
 nd|d< t }|	d|d< |	d| d< d  d| d|d dd|
 d| d|d  d S )!Nweightcurrent   deltaVsLast	series30d      iavg7dbaselineg     n@goal   i  r    pacer   i  etaWeeksz%B %-dlastWeighInz%Y-%m-%dr   zWeight updated to z	 lb (was z). Change: +.1fz lb. Pace: z lb/wk. ETA to z: ~z weeks.)
roundgetappendlensumr   maxnowdaysstrftime)r   r   oldserieslast7rB   rC   baseline_dateweeks_elapsed
total_lostrE   	remainingtodayr7   r   r   r4   <   s0   
4z/DashboardManager.update_weight.<locals>.updaterr6   r   r7   r4   _msgr   rZ   r   update_weight:   s   "zDashboardManager.update_weightc                    r8   )z*Update body fat percentage, rotate series.c                    s~   | d }|d }t  | d|d<  |d< |dg }|  t|dkr,|dd  }||d< d  d	| d
|d ddS )NbodyFatr:   r;   r<   r=   r>   r?   zBody fat updated to z% (was z%). Change: rH   z%.)rI   rJ   rK   rL   )r   bfrR   rS   rZ   r   r   r4   e   s   
z1DashboardManager.update_body_fat.<locals>.updaterr[   r\   r   rZ   r   update_body_fatc   s   z DashboardManager.update_body_fatsystolic	diastolicc                         fdd}|  |\}}|S )zUpdate blood pressure reading.c                    s   | d }|d |d }}|d<  |d< t   |d< dk r* dk r*d|d< nd	k r7 dk r7d
|d< ndk s? dk rDd|d< nd|d< d d  d|d  d| d| dS )NbloodPressurerc   rd   r   x   P   normalstatus   elevated   Z   zhigh-stage1zhigh-stage2zBP updated to / (z). Was .)r   rY   	isoformat)r   bpold_sold_drd   rc   r   r   r4   z   s   


(z7DashboardManager.update_blood_pressure.<locals>.updaterr[   )r   rc   rd   r4   r]   r^   r   rv   r   update_blood_pressurex   s   z&DashboardManager.update_blood_pressure N
event_datetimetitlelocationnoteend_datec           
         s<   ddl m m  fdd}| |\}}	|	S )zAdd event to schedule. event_date should be YYYY-MM-DD.
        If end_date is provided, creates a multi-day event spanning the range.r   )r   	timedeltac                    s   rVkrV  }  }d}|}||kr8| d}| d | |d7 }|dd7 }||ks| d jdd d d	 d
 d d| dpRd dS d}| d | | d jdd d d	 d d dp|d d	S )Nr   )r   rz   r{   r|   r}   scheduler;   )rP   c                 S      |  ddS Nr   rx   rJ   er   r   r   <lambda>       z=DashboardManager.add_event.<locals>.updater.<locals>.<lambda>)keyzAdded:  from z to rp   z days). Location: TBDrq   c                 S   r   r   r   r   r   r   r   r      r   z on z at z. Location: )fromisoformatrr   rK   sort)r   startend
days_addedr:   eventdt_dater~   ry   r|   r}   rz   r   r{   r   r   r4      s6   

("z+DashboardManager.add_event.<locals>.updater)r   r   r   r6   )
r   ry   rz   r{   r|   r}   r~   r4   r]   r^   r   r   r   	add_event   s    zDashboardManager.add_eventtitle_fragmentc                    re   )zMRemove event(s) matching title (case-insensitive). Optionally filter by date.c                    sb   t | d }   fdd| d D | d< |t | d  }|r+d| d dS d dS )Nr   c                    s4   g | ]}|d    v r du s|d  ks|qS )r{   Nr   lower.0r   )ry   title_lowerr   r   
<listcomp>   s    zBDashboardManager.remove_event.<locals>.updater.<locals>.<listcomp>zRemoved z event(s) matching ''.zNo events found matching ')rL   r   )r   beforeremovedry   r   )r   r   r4      s   
z.DashboardManager.remove_event.<locals>.updaterr[   )r   r   ry   r4   r]   r^   r   r   r   remove_event   s   zDashboardManager.remove_eventcatwhysourcec                    s$    fdd}|  |\}}|S )z,Add a curated signal to the top of the list.c                    sb    d}| d d  d| t  | d d< | d d d d | d d< d  d S )	N)r   r{   r   r   signalscuratedr   updated   zSignal added: [] )insertr   rY   rr   )r   signalr   r   r{   r   r   r   r4      s
   z,DashboardManager.add_signal.<locals>.updaterr[   )r   r   r{   r   r   r4   r]   r^   r   r   r   
add_signal   s   zDashboardManager.add_signaltextc                    r8   )zUpdate the focus/goal text.c                    s"   | d } | d< d  d| dS )NfocuszFocus updated to: 'z	' (was: 'z').r   )r   rR   r   r   r   r4      s   z.DashboardManager.update_focus.<locals>.updaterr[   )r   r   r4   r]   r^   r   r   r   update_focus   s   zDashboardManager.update_focustimingnamedosebrandc           	         s&    fdd}|  |\}}|S )z?Add a supplement. timing is 'morning', 'evening', or 'anytime'.c                    sP    d}| d vrg | d < | d   | d d d dS )N)r   r   r   r}   supplementszAdded rp   z) to z supplements.rK   )r   suppr   r   r   r}   r   r   r   r4      s
   z0DashboardManager.add_supplement.<locals>.updaterr[   )	r   r   r   r   r   r}   r4   r]   r^   r   r   r   add_supplement   s   zDashboardManager.add_supplementname_fragmentc                    r8   )zIRemove supplement matching name (case-insensitive) from any timing group.c                    s      g }dD ]5}t| d |g } fdd| d |g D | d |< |t| d |  }|r=|| d|  q|rLd dd| d	S d
 dS )Nmorningeveninganytimer   c                    s    g | ]} |d    vr|qS )r   r   )r   s
name_lowerr   r   r     s
    zGDashboardManager.remove_supplement.<locals>.updater.<locals>.<listcomp>r   zRemoved supplement 'z': z, rq   zNo supplement found matching 'r   )r   rL   rJ   rK   join)r   r   r   r   diffr   r   r   r4      s   
z3DashboardManager.remove_supplement.<locals>.updaterr[   )r   r   r4   r]   r^   r   r   r   remove_supplement   s   z"DashboardManager.remove_supplementcategoryfoodsc                    re   )zIReplace a viome food category. category: superfoods/enjoy/minimize/avoid.c                    s6    dvr
d  dS | d  < d  dt  dS )N)
superfoodsenjoyminimizeavoidInvalid category 'z+'. Use: superfoods, enjoy, minimize, avoid.viomezViome 'z' updated with z items.)rL   r   r   r   r   r   r4     s   z.DashboardManager.update_viome.<locals>.updaterr[   )r   r   r   r4   r]   r^   r   r   r   update_viome  s   zDashboardManager.update_viomefoodc                    re   )z&Add a single food to a viome category.c                    s^    | d vrd  dS | d   vr&| d     d d  dS d d  dS )	Nr   r   r   zAdded 'z' to viome rq   'z' is already in viome r   r   r   r   r   r   r4     s   z0DashboardManager.add_viome_food.<locals>.updaterr[   )r   r   r   r4   r]   r^   r   r   r   add_viome_food  s   zDashboardManager.add_viome_foodpricec              
      s   ddl } du rHz|jdddddd}|  | d d  W n& tyG } ztd	|  d
t|dd  dW  Y d}~S d}~ww  fdd}| |\}}|S )zSReset the CRO swing-trade anchor to the given price (or live price from CoinGecko).r   Nz-https://api.coingecko.com/api/v3/simple/pricezcrypto-com-chainusd)idsvs_currenciesr	   )paramsr
   zFailed to fetch CRO price: zCouldn't fetch live CRO price: rh   z;. Try again or text 'cro anchor 0.085' with a manual price.c              	      s   |  di  di  di }| dd}t d|d< | dd}| d	d
}t d|  d}t d|  d}d dd|dd| d| d	S )NcryptopositionstrategyanchorPricer      sellTriggergQ?
buyTriggerg?r;      zCRO anchor reset to $z.4fz (was $u   ).
New targets — Sell: $z	 | Buy: $z'
Dashboard will update on next refresh.)rJ   rI   )r   strat
old_anchorsell_pctbuy_pctsell_target
buy_targetr   r   r   r4   =  s   z3DashboardManager.update_cro_anchor.<locals>.updater)	requestsrJ   raise_for_statusr   r,   r*   errorr   r6   )r   r   http_requestsrespr   r4   r]   r^   r   r   r   update_cro_anchor+  s$   $z"DashboardManager.update_cro_anchorc                 C   sn   |   }|d }d|d  d|d  d|d  d|d	  d
|d  d|d  d|d  d|d  d|d  S )Nr9   zWeight: r:   z lb (7-day avg: rA   z)
Goal: rC   z lb | Baseline: rB   z lb (baselineDatez)
Pace: rE    paceUnitz	 | ETA: ~rF   z weeks
Last weigh-in: rG   r   )r   r   r   r   r   r   query_weightR  s$   zDashboardManager.query_weightc              	   C   s@   |   }|d }d|d  d|d  d|d  d|d	 d
d	S )Nr`   z
Body Fat: r:   z	% (goal: rC   z%)
Baseline: rB   z% | Change vs last: r<   rH   %r   )r   r   ra   r   r   r   query_body_fat\  s   zDashboardManager.query_body_fatc              	   C   s<   |   }|d }d|d  d|d  d|d  d|d	  S )
Nrf   zBP: rc   ro   rd   rp   rj   u   ) — measured r   r   )r   r   rs   r   r   r   query_blood_pressured  s   ,z%DashboardManager.query_blood_pressure   
days_aheadc           
   
   C   s  |   }t }g }|d D ]1}z$t|d }||  kr,|jt|j| ddkr3n n|| W q tt	fy?   Y qw |sHd| dS d| dg}|d d	 D ]1}|
d
rcd|d
  nd}|
drqd|d  nd}	|d|d  | d|d  |	  qUd|S )Nr   r      )dayzNo events in the next z days.zUpcoming events (next z days):r	   rz   r   rx   r|   z @ u   •     — r{   
)r   r   rY   r   r)   minr   rK   
ValueErrorKeyErrorrJ   r   )
r   r   r   rY   upcomingr   edatelinestime_strloc_strr   r   r   query_schedulei  s,   
(
zDashboardManager.query_schedulec              	   C   sx   |   }|d }dg}dD ])}||g }|r6|d|  d |D ]}|d|d  d|d	   q$qd|S )
Nr   zSupplements:r   r   :u     • r   r   r   )r   rJ   rK   upperr   )r   r   r   r  r   itemsitemr   r   r   query_supplements  s    
z"DashboardManager.query_supplementsc                 C   sd   |   }|d d d d }d|d d  dg}|D ]}|d|d  d	|d
   qd|S )Nr   r      zLatest signals (updated r   z):[r   r   r{   r   )r   rK   r   )r   r   sigsr  r   r   r   r   query_signals  s    
zDashboardManager.query_signalsc                 C   s   |   }|d }|d }|d }tdd |d D }d|d  d	|d
  d|d  d|d  d|d
  d|d  d|d  d|d  d|d  d|d  d| dt|d d  dS )zFull dashboard summary.r9   r`   rf   c                 S   s(   g | ]}| d dt  kr|qS )r   rx   )rJ   r   rY   rr   r   r   r   r   r     s
    z2DashboardManager.query_summary.<locals>.<listcomp>r   zDashboard Summary (r   z):
Weight: r:   z
 lb (goal rC   z
) | Pace: rE   z lb/wk
Body Fat: z% (goal z%)
BP: rc   ro   rd   rp   rj   z	)
Focus: r   z
Upcoming events: z

Signals: r   r   z curated)r   rL   )r   r   r   ra   rs   sched_countr   r   r   query_summary  s>   zDashboardManager.query_summary)rx   rx   N)N)rx   )rx   rx   )r   )__name__
__module____qualname__r   r   dictr   r3   r6   floatr_   rb   intrw   r   r   r   r   r   r   listr   r   r   r   r   r   r  r	  r  r  r   r   r   r   r      sR    )
* 
'
r   )__doc__r   r&   r$   loggingr   r   pathlibr   filelockr   r   	getLoggerr  r*   r   r   r   r   r   <module>   s    
