
    bMjD                       U d Z ddlZddlZddl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ZddlZddlmZ ddlmZ ddlmZ  ej,                  d      Z ej0                         Z e       Zee   ed	<   d
eddfdZd
edefdZ d
eddfdZ! ee"      jG                         jH                  dz  Z% ee"      jG                         jH                  dz  Z& ee"      jG                         jH                  dz  Z' ejP                  d      Z)dZ*dZ+dZ,dZ-ddddddddddddddg dd d!
Z.e/ed"<   de/fd#Z0d$ede/fd%Z1d&d'g g g d(Z2e/ed)<   d*d+d,d-d.d,d/d0d,d1d2d,d3d4d,d5d6d,gZ3e4e/   ed7<   de/fd8Z5d9e/de/fd:Z6defd;Z7d<eddfd=Z8d>ede/fd?Z9de4fd@Z:dd>edz  de/fdAZ;ddBe/d>edz  ddfdCZ<dDe=defdEZ>dFe/dDe=d3ed5ede/f
dGZ?dHdIdJedKe/dLedefdMZ@dNe/dKe/deAeef   fdOZBde/fdPZCdQedFe/dRe/dSedTedeAdUeef   fdVZDdWedefdXZEdYefdZZFd[edefd\ZGd[ed]e/de4e   fd^ZHd_e4d]e/de4eAeef      fd`ZIdeAe=daf   fdbZJd_e4d]e/de4e/   fdcZKde/fddZLdddedfdgdhdFdUd]dUdieMdjeMdkeMde/fdlZNdmedefdnZOdoedmedpedefdqZPdredeMfdsZQddtdueRdvedwdxdeAeRe/f   fdyZSdze4e/   dd{fd|ZTdueRdFe/d]e/doeddUf
d}ZUdQed5ed*edefd~ZVde/dRe/deAeef   fdZWg dZXde/d3ededefdZYde/d3ededefdZZ	 	 dd3eded
edz  d>edz  dede/fdZ[	 	 dd3eded
edz  d>edz  dede/fdZ\e]dk(  rddl^Z^ddlZ_ddl`Z` ej                  ej                  d        e^j                  d      Zdedj                  ddd       edj                         Zg e[egj                        Zi ej e_j                  eide              eeij                  d      xs eij                  d            Zm e`j                  emrdnd       yy)uO	  
winston_pipeline.py — Winston Linux quote pipeline (DVI-815).

Port of the legacy outlook_auto/main.py + clickup_quotes.py to run on
fileshare.icastinc.com (Linux), using app-only Graph (winston_graph.py) and
the B1 convert-to-PDF recipe proven in DVI-807. No Windows/Excel COM required.

Entry point: run(date='YYYY-MM-DD')
  Callable from the P2 scheduler and the P4 "Run now" UI button.

Pipeline steps per quote task
------------------------------
1. ClickUp pull      — fetch quote tasks matching target date
2. Find + download   — locate the .xlsm workbook in OneDrive by quote number
3. openpyxl extract  — resolve quote sheet(s) from ClickUp tags via tag_map,
                        trim empty rows + dynamic print area, read grand total
                        by label search (DVI-982); build one trimmed single-sheet
                        .xlsx per matched sheet (DVI-983 — often 2+ per quote)
4. Graph convert     — upload each .xlsx to scratch drive, GET ?format=pdf; one
                        PDF per matched sheet, named {quote}{sheetLabel}{ver}.pdf
5. Mail draft        — one draft in sales mailbox with ALL the quote's PDFs
                        attached (idempotent: skip if exists)
6. ClickUp writeback — attach each PDF to task + set Quote Amount custom field
                        (single field = summed grand total)
7. Routed PDF upload — upload each PDF to OneDrive (owner personal / shared path)

Configuration (env vars, with fallback to winston_settings.json)
-----------------------------------------------------------------
  CLICKUP_API_KEY               ClickUp personal API token
  CLICKUP_LIST_ID               ClickUp list ID to query for quote tasks
  CLICKUP_DATE_FIELD            Task date field to filter on (default: due_date)
  CLICKUP_QUOTE_AMOUNT_FIELD_ID Custom field ID for Quote Amount (auto-detected if absent)
  WINSTON_ONEDRIVE_USER         UPN of the OneDrive owner holding Takeoffs folder
  WINSTON_TAKEOFFS_FOLDER       OneDrive path to the .xlsm workbooks (see default below)
  WINSTON_SERVICE_USER          UPN for the scratch drive used during xlsx→PDF conversion
  WINSTON_MAIL_FROM             Draft mailbox UPN (default: sales@icastinc.com)
  AZURE_CLIENT_ID               } App-only Graph credentials (already wired in DVI-806)
  AZURE_CLIENT_SECRET           }
  AZURE_TENANT_ID               }
    N)datetimetimezone)Path)get_column_letter)winston_graph)ClickUpClientztogen.winston_pipeline_cancel_requestedrun_idreturnc                 d    t         5  t        j                  |        ddd       y# 1 sw Y   yxY w)zHFlag a run for cooperative cancellation (called from the stop endpoint).N)_cancel_lockr	   addr
   s    '/var/www/html/togen/winston_pipeline.pyrequest_cancelr   J   s(    	 &f%& & &   &/c                 J    t         5  | t        v cd d d        S # 1 sw Y   y xY wN)r   r	   r   s    r   is_cancelledr   P   s#    	 +**+ + +s   "c                 d    t         5  t        j                  |        d d d        y # 1 sw Y   y xY wr   )r   r	   discardr   s    r   _clear_cancelr   U   s(    	 *!!&)* * *r   zwinston_settings.jsonuser_settingszwinston_email_template.jsonz(?i)^send\s+quotes?\s+\d))   i  )i  i!     '   z&Infrastructure Precast Inc_ - Takeoffssales@icastinc.com 
9014248643QuoteXQuoteYQuoteZLP)XYZr#   )SPCACBWBCBRESEMHRWSHWQWW)customstandard)
draft_mailboxclickup_api_keyclickup_list_idclickup_team_idclickup_date_fieldonedrive_usertakeoffs_foldershared_quotes_locationshared_quote_takeoff_locationtag_map_WINSTON_DEFAULTSc                  "   t        t              } t        j                         rK	 t	        j
                  t        j                               }t        |t               r| j                  |       | S | S # t        j                  t        f$ r Y | S w xY wr   )dictr>   _SETTINGS_FILEis_filejsonloads	read_text
isinstanceupdateJSONDecodeErrorOSError)settingssaveds     r   _load_settingsrL      sz    %&H	JJ~779:E%&& O8O $$g. 	O	s   AA1 1BBemailc                 0   t        j                  dd| j                               }t        | dz  }|j	                         r4	 t        j                  |j                               j                  di       S i S # t
        j                  t        f$ r Y i S w xY w)zEReturn the 'documents' sub-dict from a user's personal settings JSON.z[^a-zA-Z0-9@._-]_z.json	documents)resublower_USER_SETTINGS_DIRrB   rC   rD   rE   getrH   rI   )rM   safefps      r   _load_user_docsrX      s    66%sEKKM:D	en	,B	zz|	::blln-11+rBB I $$g. 	I	s   2A8 8BBu-   Quote {quote_number} — iCast Infrastructurea   <p>Please find attached the quote for <strong>{quote_number}</strong>.</p><p>Quote Total: <strong>{quote_total}</strong></p><p>This quote was prepared by iCast Infrastructure. Please review and let us know if you have any questions.</p><p>Thank you,<br>iCast Infrastructure Sales Team</p>)subject	body_htmlccbccfallback_toEMAIL_TEMPLATE_DEFAULTSquote_numberu7   ClickUp task — the quote number (workbook / PDF name))tokensourcequote_totalz\Grand total read by label from the resolved quote sheet, currency-formatted (e.g. $1,234.56)recipient_emailu)   ClickUp task — the quote's To recipientowner_emailu.   ClickUp task — the quote owner / salespersondatez%Pipeline run target date (YYYY-MM-DD)mailboxu7   Configured send-as mailbox (Settings → Draft mailbox)EMAIL_TOKENSc                      t         j                         r=	 t        j                  t         j	                               } t        | t              r| S 	 i S i S # t        j                  t        f$ r Y i S w xY w)z@Return the raw on-disk store (global fields + pipelines + flag).)	_EMAIL_TEMPLATE_FILErB   rC   rD   rE   rF   r@   rH   rI   )rK   s    r   _load_email_storerj      sn    ##%	JJ3==?@E%& ' I2I $$g. 	I	s   8A A10A1storec                     t        t              }t        j                         D ],  \  }}|| v st        | |         t        |      u s%| |   ||<   . |S )zLResolve the global template = DEFAULTS overlaid with saved top-level fields.)r@   r^   itemstype)rk   tplkdefaults       r   _global_templaterr      sW    
&
'C-335 
7:$uQx.DM91XCF J    c                  F    t        t               j                  d            S )z3Whether per-pipeline email templates are turned on.per_pipeline_enabled)boolrj   rU    rs   r   ru   ru      s    !#''(>?@@rs   enabledc                     t               }t        |       |d<   t        j                  t	        j
                  |d             y )Nru      indent)rj   rv   ri   
write_textrC   dumps)rx   rk   s     r   set_per_pipeline_enabledr      s2    E$(ME
 !##DJJuQ$?@rs   pipeline_idc                     t               j                  d      }t        |t              r|j                  |       nd}t        |t              rt        |      S i S )zEReturn the sparse override dict saved for a pipeline (empty if none).	pipelinesN)rj   rU   rF   r@   )r   r   ovs      r   get_pipeline_overrider      sF    !#''4I'1)T'B{	#B!"d+4833rs   c                      t               j                  d      } t        | t              rt	        | j                               S g S )z?Pipeline ids that currently have a saved per-pipeline override.r   )rj   rU   rF   r@   listkeys)r   s    r   pipeline_override_idsr     s5    !#''4I%/	4%@4	 !HbHrs   c                 d   t               }t        |      }| r|j                  d      r|j                  d      }t        |t              r|j                  |       nd}t        |t              rCt
        j                         D ],  \  }}||v st        ||         t        |      u s%||   ||<   . |S )ai  Resolve the effective email template for an optional pipeline.

    Returns the global template, overlaid per-field with any saved override for
    ``pipeline_id`` (Option C). The overlay only applies when the per-pipeline
    feature is enabled AND a (non-empty) override exists for that field;
    otherwise the single global template is used (fallback).
    ru   r   N)rj   rr   rU   rF   r@   r^   rm   rn   )r   rk   ro   	overridesr   rp   rq   s          r   load_email_templater   	  s     E
5
!Cuyy!78IIk*	+5i+FY]];'Db$5;;= #
77tBqE{d7m;UCF# Jrs   ro   c                 n   t               }|r|j                  d      }t        |t              si }i }t        j                         D ]`  \  }}|| vst        | |         t        |      ur$| |   }t        |t              r|j                         rt        |t              r|s\|||<   b |r|||<   n|j                  |d       ||d<   nCt        j                         D ],  \  }}|| v st        | |         t        |      u s%| |   ||<   . t        j                  t        j                  |d             y)aX  Persist template fields.

    When ``pipeline_id`` is None the fields update the single global template.
    When set, the fields are stored as a SPARSE override for that pipeline:
    empty strings / empty lists are dropped so those fields keep inheriting the
    global template. An override with no non-empty fields is removed entirely.
    r   Nrz   r{   )rj   rU   rF   r@   r^   rm   rn   strstripr   popri   r}   rC   r~   )ro   r   rk   r   overriderp   rq   vals           r   save_email_templater     s"    EIIk*	)T*I1779 	JAw|tCF|4=@a&C3$SYY[jd>S\_HQK	 %-Ik"MM+t,&k1779 	"JAwCxDQLDM9q6a	" ##DJJuQ$?@rs   grand_totalc                 X    t        | t        t        f      rd| dS | t        |       S dS )N$z,.2f   —)rF   intfloatr   )r   s    r   _format_totalr   >  s5    +U|,;t$%%*63{AEArs   taskc                     | j                  dd      t        |      | j                  dd      | j                  dd      |xs d|xs ddS )z@Map pipeline source data to the substitution tokens for a quote.r_   r   rc   rd   r_   rb   rc   rd   re   rf   )rU   r   )r   r   re   rf   s       r   build_email_contextr   D  sO     4$[188$5r:xxr2
=b rs   Fescapetextcontextr   c                Z    dddt         ffd}t        j                  d|| xs d      S )u%  Substitute {token} placeholders from context.

    Unknown tokens are left untouched (so stray braces in authored HTML/CSS are
    preserved). When escape=True, substituted values are HTML-escaped — used for
    the body, where values come from external data. Subject uses escape=False.
    mzre.Matchr   c                     | j                  d      }|v r't        |         }rt        j                  |      S |S | j                  d      S )N   r   )groupr   _htmlr   )r   keyr   r   r   s      r   _replzrender_template.<locals>._replW  sI    ggaj'>gcl#C(.5<<$7C7wwqzrs   z	\{(\w+)\}r   )r   rQ   rR   )r   r   r   r   s    `` r   render_templater   P  s/       66,tzr22rs   templatec                     t        | j                  dd      |      xs t        t        d   |      }t        | j                  dd      |d      xs t        t        d   |d      }||fS )z6Render (subject, body_html) from a template + context.rY   r   rZ   Tr   )r   rU   r^   )r   r   rY   rZ   s       r   render_emailr   a  sr    hll9b97C H29=wG [" =wtT W2;?QUV Irs   c                  t    ddddt        j                  t        j                        j	                  d      ddS )zFA representative context for the Email tab 'Preview with sample data'.z26-2904z
$48,750.00zcustomer@example.comzjsmith@icastinc.comz%Y-%m-%dr   r   )r   nowr   utcstrftimerw   rs   r   sample_email_contextr   j  s8     "#1,X\\*33J?' rs   r`   rJ   r9   r:   zdict | Nonec                    |d   }|j                  dd      j                         }|j                  dd      j                         }|xs |}|r,t        j                  | ||      }	|	r|	t        j                  |fS |rLt        |      }
|
j                  dd      j                         }|rt        j                  | |||      }	|	r|	||fS y)u  Locate the xlsm workbook using shared-first → personal-fallback rule.

    Search order:
      1. Shared Quote Take-offs (shared_quote_takeoff_location; falls back to
         the legacy takeoffs_folder when the new field is empty).
      2. Owner's personal Quote Take-offs (quote_takeoff_location from their
         User Settings Documents tab) — only tried when the owner email is
         known and the personal path is configured.

    Returns (item, user_upn, folder_path) on success, or (None, '', '') if the
    workbook was not found in either location.  Existing behavior is preserved
    when shared_quote_takeoff_location is empty (falls back to takeoffs_folder).
    r_   rd   r   r<   quote_takeoff_location)Nr   r   )rU   r   r   site_find_quote_workbookSITE_DRIVE_UPNrX   find_quote_workbook)r`   r   rJ   r9   r:   qnrd   shared_takeoffprimary_folderitemdocspersonal_takeoffs               r   _winston_find_workbookr   y  s      
n	B((="-335K
 \\"A2FLLNN#6N55e^RP55~EE {+88$<bAGGI 44UKIY[]^D[*:::rs   namec                 J    | xs dj                  dd      j                         S )zECase- AND whitespace-insensitive key for sheet matching (PoC parity).r    )replacerS   )r   s    r   _normalize_sheet_keyr     s"    JBR(..00rs   targetc                 f    t        |      }| j                  D ]  }t        |      |k(  s| |   c S  y)zIReturn the worksheet matching `target` case/space-insensitively, or None.N)r   
sheetnames)wbr   r   r   s       r   _find_sheetr     s:    
v
&C %,d8O rs   tagc                     t        j                  d| xs dj                         t         j                        }|syt	        |j                  d            }|dk  rdS d| S )u   PDF version suffix for a tag: '' for v1/none, 'v2' for v2, … (PoC parity).

    (Consumed by the DVI-983 multi-PDF naming; surfaced here so the resolver
    carries it alongside each candidate.)
    z\s*v(\d{1,2})$r   flagsr   v)rQ   searchr   
IGNORECASEr   r   )r   r   ns      r   _extract_version_suffixr     sV     			#ciR%6%6%8NAAGGAJAa2$qW$rs   r=   c                    ddl m}  ||       j                         j                         }|sg S |j	                  d      xs i }|j                         D ]C  \  }}t        |      j                         |k(  s#t        |t              r|gc S t        |      c S  |j	                  d      xs g D ].  }t        |      j                         |k(  s | d| d| dgc S  g S )u  Resolve one ClickUp tag (optional vN suffix) to candidate sheet names.

    Mirrors PoC run_macros.get_sheets_for_tag against the winston_settings
    tag_map shape ``{"custom": {TAG: sheet}, "standard": [TAG, …]}``:
      - a custom tag  → its single fixed sheet name
      - a standard tag → three candidates ``"{TAG} Quote 1/2/3"``
    Lookup is case-insensitive; candidate casing follows tag_map so it matches
    real tab names under the space/case-insensitive sheet lookup.
    r   strip_version_suffixr2   r3   z Quote 1z Quote 2z Quote 3)	togen.clickup_clientr   r   upperrU   rm   r   rF   r   )r   r=   r   
base_upperr2   r   r   std_tags           r   get_sheets_for_tagr     s     :%c*00288:J	[["(bFLLN @Ss8>>z)&sC0C5?d3i?@ KK
+1r Vw<:-ix(WIX*>7)8@TUUV Irs   tagsc                     g }t               }| xs g D ]G  }t        |      }t        ||      D ]+  }||vs|j                  |       |j	                  ||f       - I |S )zOrdered, de-duplicated ``(sheet_name, version_suffix)`` candidates for a
    task's collected tags (mirrors PoC build_quote_sheet_map inner loop).)setr   r   r   append)r   r=   entriesseenr   versheets          r   candidate_sheet_entriesr     sm     &(GUDzr -%c*'W5 	-ED s|,	-- Nrs   z
str | Nonec                 B   | j                         D ]  }|D ]  }|j                  }t        |t              s d|j	                         j                         v sA| j                  |j                  dz   |j                        j                  }||j                  fc c S   y)u  Find the cell containing 'grand total' (case-insensitive, PARTIAL match)
    and return ``(value_of_cell_below, label_cell_coordinate)``.

    Mirrors PoC ``Cells.Find("grand total", LookAt=xlPart)`` → the cell below.
    Returns ``(None, None)`` when no such label is present.
    zgrand totalr   rowcolumn)NN)
	iter_rowsvaluerF   r   r   rS   cellr   r   
coordinate)wsr   r   r   belows        r   _find_grand_totalr     s     ||~ . 	.D

A!S!mqwwy7H&HDHHqLEKKdoo--		.. rs   c           	         g }t               }t        ||      D ]n  \  }}t        | |      }|t        |j                        }||v r/|j                  |       t        |      \  }	}
|j                  ||j                  ||	|
d       p |S )u  Resolve a task's tags to the quote sheets that ACTUALLY exist in `wb`.

    Returns one dict per matched existing sheet, in candidate order::

        {candidate, matched_name, version_suffix, grand_total, grand_total_cell}

    Sheet matching is case/space-insensitive; the grand total is read per sheet
    by label search. An empty list means no candidate resolved (caller emits a
    diagnostic and skips the quote — no crash).
    )	candidatematched_nameversion_suffixr   grand_total_cell)r   r   r   r   titler   r   r   )r   r   r=   resolvedmatched_keysr   r   r   r   totalr   s              r   resolve_quote_sheetsr     s     H UL1$@ 	3Y':"288,,'+t"HH!  $
 	  Ors   c                    d}t               }t        D ]|  \  }}t        ||dz         D ]e  }| j                  |t              j
                  }	 |du xs t        |      dk  }|| j                  |   _        |sP|dz  }|j                  |       g ~ d}t        | j                   xs d      }	t#        t        | j$                  xs d      t&              }
t        d|	dz         D ]_  }||v rt        d|
dz         D ]F  }| j                  ||      j
                  }|#t        |t              r|j                         sD|} _ a | j(                  }|r t        |t              r|n
t        |      }d}n@t#        t        | j$                  xs d      t&              }dt+        |       | }|| _        d}||||dS # t        t        f$ r- |du xs# t        |t              xr |j                          }Y w xY w)	a  Hide empty line-item rows and set a dynamic print area (DVI-982).

    (a) Hide rows in the trim bands whose column O (15) is blank or < 1 via
        openpyxl ``row_dimensions[r].hidden = True`` (mirrors PoC
        hide_rows_via_com over the same bands/column).
    (b) Compute a print area whose bottom is the last VISIBLE populated row so
        the PDF is still trimmed if the Graph export ignores hidden rows.

    Returns a diagnostics dict ``{hidden_rows, last_row, print_area}``.
    r   r   r   Nr   zA1:	synthetic)hidden_rowslast_row
print_areaprint_area_source)r   _TRIM_ROW_BANDSranger   _TRIM_CHECK_COLr   r   	TypeError
ValueErrorrF   r   r   row_dimensionshiddenr   r   max_rowmin
max_column_PRINT_AREA_LAST_COLr   r   )r   r   
hidden_setbeginendrr   should_hider   r  max_colcr   existingr   r   last_cols                    r   _trim_rows_and_print_arear    s     K5J% 
"
sucAg& 		"AAo6<<AR4i758a< +6Ba 'q q!		"
" H"**/"G#bmm(q)+?@G1gk" 
?q'A+& 	A''a'*00C
3(<SYY[		  }}H!+Hc!:XH
#s2==-A.0DE,X67zB
"'&H$;LN NK z* R4iQJq#,>,Pqwwy=Rs   F::8G65G6         )r   r=   sample_rowssample_colscell_capr  r  r  c          
      X   g }| j                   D ]  }| |   }	 t        |j                  xs d      }	t        |j                  xs d      }
g }t        dt        ||	      dz         D ]  }g }t        dt        ||
      dz         D ]U  }|j                  ||      j                  }|dn
t        |      }t        |      |kD  r|d| dz   }|j                  |       W |j                  |        |j                  ||	|
|d        |t        | j                         d}||dd	lm} |j                  d
      xs g }t!        ||      }t#        | ||      }|D cg c]0  }| ||      j%                         t'        |      t)        ||      d2 }}| j                   D ci c]  }t+        |      | }}|D ch c]  }t+        |d          }}g }|D ]B  \  }}t+        |      }|j                  |      }|j                  |||du||duxr ||v d       D g } |D ]|  }|d   }!d}"|!rQt-        j.                  dt        |!            }#|#r0|#j1                  d       t        |#j1                  d            dz    }"| j                  |d   |d   |d   |!|"d       ~ |||| t3        |      d|d<   ||d
<   |D $cg c]  \  }}$|	 c}$}|d<   |D cg c]  }|d   |d   |d   d c}|d<   t3        |      |d<   |S # t        $ r dx}	}
Y w xY wc c}w c c}w c c}w c c}$}w c c}w )u  Summarize a loaded workbook for the run-diagnostics inspector (DVI-948).

    For every sheet: name, max_row×max_col dimensions, and a top-left cell grid
    (sample_rows × sample_cols of cached values). When ``task`` + ``tag_map`` are
    provided (DVI-982), also report the tag-driven sheet resolution: the base
    tags, candidate sheet names, and the resolved existing sheets with the grand
    total read by label per sheet.
    r   r   r   Nr   u   …)r   r  r  sample)sheetssheet_namesr   r   )r   baser   
candidatesr   )r   r   existsr   chosenr   z([A-Za-z]+)(\d+)$rz   r   r   )r   r   r   grand_total_label_cellgrand_total_value_cell)r   tag_resolutionr  chosen_sheetsmatched
resolutioncandidate_sheets)r   r   r   resolved_sheetsr%  )r   r   r  r  	Exceptionr   r  r   r   r   lenr   r   r   r   rU   r   r   r   r   r   r   rQ   matchr   rv   )%r   r   r=   r  r  r  r  r   r   r  r  gridr  row_valsr  r   sresultr   r   r  r   r   r#  r   existing_keyschosen_keyscandidate_statuscandr   rp   r   r$  
label_cell
value_cellr   rO   s%                                        r   sample_workbook_structurer6  Y  s    F X	""**/*G"--,1-G !#q#k73a78 	"A"$H1c+w7!;< #GG!G,22)BQq6H$)8u,A"# KK!	" 		
 	#2 BMM*F G/=xx%2,T7;
'D': 
  ,S1779"9#">0g>	
 
 >@]]K-a0!3KKHPQ1+An,=>QQ# 		ID#$T*A(,,Q/L##!"%&d2 ,&d2GqK7G% 		  	A-.JJHH13z?C$%GGAJ<AGGAJ!0C/D!EJ  >*"#$4"5 /*4*4" 	 ,**H~ 
| v4>%?DAqa%?!" %
 	 >* /$%&8$9%
 ! !NyM}  	" !!Gg	"@
 LQN &@%
s/   2K>5L!L:L;L!L'>LL
sheet_namec                     t        j                  dd| xs dt         j                        }t        j                  dd|t         j                        }|S )u"  Derive the PoC PDF sheet-label from a matched sheet name.

    "BW Quote 1" → "BW"  (standard tags: strip trailing " Quote N")
    "QuoteX"     → "X"   (custom tags:   strip leading "Quote" before a letter)
    Mirrors PoC ``run_macros.process_single_excel_file`` label derivation.
    z\s+Quote\s+\d+$r   r   z^Quote(?=[A-Za-z]))rQ   rR   r   )r7  labels     r   
_pdf_labelr:    s@     FF%r:+;2==QEFF("e2==IELrs   	quote_numr   c                 &    |  t        |       | dS )u8  PoC-style per-sheet PDF name ``{quote}{sheetLabel}{versionSuffix}.pdf``.

    e.g. quote ``26-2270`` + sheet ``QuoteY`` + tag ``yv2`` → ``26-2270Yv2.pdf``.
    (Mirrors PoC exactly: the version suffix carries no leading underscore — see
    ``_extract_version_suffix`` / PoC ``extract_version_suffix``.)
    z.pdf)r:  )r;  r7  r   s      r   _pdf_filenamer=    s      [J/00@EErs   keepc                     d}	 | j                   }t        |d      r4t        t        |j	                                     }	 |j                          nWt        |d      rKt        |j                        }t        |j                        D ]  }	 |j                  |j                           | j                  D ]Y  }| |   }t        |dd      }|	 t        |d      r5|t        t        |j	                                     z  }|j                          [ |rt        j                  d||       |S # t        $ r  t        |             | _         Y w xY w# t        $ r Y w xY w# t        $ r t        j                  d       Y w xY w# t        $ r Y w xY w)u[  Remove workbook- and sheet-level defined names that make the Graph→Office
    converter return 406 UnsupportedMediaType (DVI-1003).

    The 2026 template ships Excel-internal ``_xleta.*`` LAMBDA/eta helper names
    that the converter rejects. Since the workbook is loaded ``data_only=True``
    (formulas baked to static values), no cell references any defined name, so
    clearing them is safe. Print area / print titles are worksheet-level in
    openpyxl (``ws.print_area``) and are NOT touched here. Returns the count of
    names removed. Version-robust across openpyxl defined-name APIs.
    r   r   definedNamez1_strip_defined_names: workbook-level clear faileddefined_namesNz7_strip_defined_names: removed %d defined name(s) for %r)rA  hasattrr*  r   r   clearr)  rn   r@  deleter   log	exceptionr   getattrinfo)r   r>  removeddndr   r   wdns           r   _strip_defined_namesrM    st    GK2v$rwwy/*G.
 R'"..)G"..) IIaff%  
Xb/40;	sF#3tCHHJ/00		
 JGUYZN5  .#+48: . !  KIJK  		sk   :E D* :E 
E%E AF*EE 
EE 	EE EE E>=E>	FFquote_sheet_names
xlsm_bytestarget_namerO  zset[str] | Nonec                @   t        j                  t        j                  |       dd      }t	        ||      xs ||   }|j
                  }|Q|j                  D cg c]  }||k7  s	||vs| }}|D ]  }||=  |r!t        j                  dt        |      ||       t        ||       |j                  D ]  }||k(  rdnd||   _         ||   |_        t        ||         }	t        j                         }
|j                  |
       |
j                         |	fS c c}w )u  Produce a single-visible-sheet, trimmed .xlsx for ONE resolved quote sheet.

    Loads a fresh workbook from the original bytes (so each per-sheet export is
    independent and non-destructive to siblings), then:
    - Makes the target sheet VISIBLE and trims its empty rows.
    - HIDES other quote sheets (siblings that share the quote pattern).
    - DELETES all non-quote helper sheets (Database, Data Trans., input, etc.).
    - STRIPS workbook-level defined names (the real 406 trigger, see below).

    Why strip defined names (DVI-1003 — the actual 406 root cause):
    The Graph→Office converter returns ``406 UnsupportedMediaType`` when the
    workbook carries certain Excel-internal defined names — notably the
    ``_xleta.*`` LAMBDA/eta-reduction helpers that the 2026 template introduces
    (e.g. ``_xleta.IF``, ``_xleta.TYPE``). This — NOT file size — is why the 2026
    template failed while the smaller 2023 template (DVI-985) converted cleanly.
    A live convert test on the exact failing file (26-2819Yv2.xlsx) proved it:
    clearing defined names → HTTP 200 clean PDF at any size (1.3 MB and 9.9 MB
    both converted); leaving them → 406 even at 0.17 MB. Because the workbook is
    loaded ``data_only=True`` (all formulas already baked to static values), NO
    remaining cell references a defined name, so clearing them is safe. Per-sheet
    print area / print titles live at the worksheet level (``ws.print_area``),
    not in ``wb.defined_names``, so layout fidelity (DVI-985) is preserved.

    Why also delete helper sheets (DVI-1001): keeps the output small so the Graph
    upload stays fast/simple; combined with data_only-baked values there are no
    live cross-sheet references, so deletion is non-destructive.

    ``quote_sheet_names`` should be the full set of resolved quote-sheet titles
    for this quote (from :func:`resolve_quote_sheets`), including ``target_name``.
    Pass ``None`` to fall back to the old behavior (hide all non-target sheets).
    TF	data_onlykeep_vbaz6_build_sheet_xlsx: deleted %d helper sheets for %r: %svisibler  )openpyxlload_workbookioBytesIOr   r   r   rE  rH  r*  rM  sheet_stateactiver  savegetvalue)rP  rQ  rO  r   r   r>  r   	to_deleter   trimbufs              r   _build_sheet_xlsxrb    s   J 
		

: 6$QV	WB	R	%	8KB88D$ "Z1dqHY?YQZ	Z 	A1	HHM^T96 T" G,0DLyh4G4BI$RX.D
**,CGGCL<<>4 [s   
D&D+Dr   zfloat | Nonec           	         g }| D ]  }|j                  d      }t        |t              r%t        |t        t        f      r|j                  t	        |             Vt        |t              sg	 |j                  t	        |j                  dd      j                  dd      j                                       |rt        t        |      d      S dS # t        $ r Y w xY w)zQuote grand total = sum of the numeric per-sheet grand totals (PoC parity;
    one sheet in the common case, so just that sheet's total). Returns ``None``
    when no sheet yielded a numeric total.
    r   r   r   ,rz   N)rU   rF   rv   r   r   r   r   r   r   r  roundsum)r   valsr  r   s       r   _sum_grand_totalsrh  N  s    
 D 
EE- aa#u&KKa!3E!))C"4"<"<S""E"K"K"MNO
 #'5TA0D0  s   -AC	CCc                    |j                  d      xs g }t        | ||      }|sy|D ch c]  }|d   	 }}g }	|D ]  }|d   }
|d   }t        ||
|      \  }}t        ||
|      }|	j	                  |
||d   |d   t        |
      |||d       t        j                  d	|
||d   |d   |d
   |d   t        |      dz          t        |      }|	||dS c c}w )u  Resolve the quote sheet(s) from tags and build ONE trimmed xlsx per sheet.

    The real workflow emits one PDF per matched tag/sheet (often 2+), so this
    returns a list of per-sheet artifacts (DVI-983) rather than a single one.

    Returns ``None`` when NO candidate sheet resolves (the caller emits a clear
    diagnostic and skips the quote — the old hard ``ValueError`` failure path is
    gone). Otherwise returns::

        {
          "sheets": [ {sheet_name, version_suffix, grand_total, grand_total_cell,
                       pdf_label, pdf_filename, xlsx_bytes, trim}, … ],
          "grand_total": <summed numeric total | None>,   # ClickUp Quote Amount
          "resolved": <raw resolve_quote_sheets list>,
        }

    Sheets are deduped by resolved name (in :func:`resolve_quote_sheets`); each
    per-sheet xlsx is built from a fresh reload of ``xlsm_bytes`` so building one
    never mutates the workbook used for another.
    r   Nr   r   rN  r   r   )r7  r   r   r   	pdf_labelpdf_filename
xlsx_bytesr`  zV_extract_from_workbook: sheet=%r pdf=%s total=%r cell=%s hidden=%d print=%s xlsx=%d KBr   r      )r  r   r   )
rU   r   rb  r=  r   r:  rE  rH  r*  rh  )r   rP  r   r=   r;  r   r   r  all_quote_sheet_namesr  r7  r   rl  r`  rk  r   s                   r   _extract_from_workbookro  b  s1   . 88F!rD#Bg6H8@A1Q~.AAF 
~&
+,,
6K

D %Y
NK$,]+ !"4 5#J/($	
 		 	da&6:L8Ml!3S_5L	
#
. $H-K" 5 Bs   Cc                    t         j                   d| d}|j                  dd      }d| dddd	}	 t        j                  |d
|  dd|d      }|j
                  dk(  r6|j                         j	                  d      rFt        j                  d|       yt        j                  d|j
                  ||j                  dd        y# t        $ r t        j                  d|       Y yw xY w)zReturn True if the sales mailbox Drafts folder already has a message
    whose subject contains quote_number (Graph OData contains() filter).
    z/users/z/mailFolders/drafts/messages'z''zcontains(subject,'z')z
id,subject1)z$filterz$selectz$topzBearer zapplication/json)AuthorizationzContent-Type   )headersparamstimeoutr  r   u1   Draft already exists for %s — skipping (dedupe)Tz+_draft_exists: Graph returned %s for %s: %sNz _draft_exists error for quote %sF)r   _GRAPH_BASEr   _reqrU   status_coderC   rE  rH  warningr   r)  rF  )r`   rf   r_   urlsafe_qnrv  r  s          r   _draft_existsr~    s     &&'wwi7S
TC""3-G'y3F
HHH(/w&7I[\	
 ==Cvvx||G$Ll[KK=t	   H8,GHs   A#C /C C*)C*quotec                    | j                  dd      j                         }|r1t        |      }|j                  dd      j                         }|r||fS |j                  dd      j                         }t        j                  |fS )u  Return (user_upn, folder_path) for the completed PDF upload (Completed Quotes).

    Rule (board decision 2 — DVI-805, updated DVI-843):
      If the quote owner has a personal Completed Quotes path (quotes_location)
      in their User Settings Documents tab → route there (their OneDrive).
      Else → shared Completed Quotes path (shared_quotes_location) in Togen Admin.

    The routing *criteria* (owner-based: personal overrides shared) is isolated
    here so it can be swapped without touching the rest of the pipeline.
    rd   r   quotes_locationr;   )rU   r   rX   r   r   )r  rJ   rd   r   personal_pathshared_paths         r   _winston_route_destinationr    s     ))M2.446K{+!2B7==?--
 ,,7<BBDK''44rs   ))quotes_foundzQuotes found)drafts_createdzDrafts created)drafts_skippedz Drafts skipped (already existed))drafts_failedzDrafts failed)clickup_amounts_setzClickUp amounts set)clickup_amounts_failedzClickUp amounts failed)clickup_attachedzClickUp PDFs attached)clickup_failedzClickUp attach failed)
pdf_routedzPDFs routed to OneDrive)pdf_route_failedzPDF routing failed)writes_skipped_dry_runzWrites skipped (dry run)summarytriggerc                     t         j                  d            }t        j                  d}|rd}g } j                  d      r,|j	                  d dj                   d               z           j                  d      r&|j	                  d t         d               z           j                  d	      r|j	                  d
       d}|rddj                  |      z   dz   }dj                   fdt        D              } j                  d      xs g }d}	|D ]  }
 t        |
j                  d|
j                  dd                        } t        |
j                  dd      xs d            } t        |
j                  dd                  } t        |
j                  dd                  }|
j                  d      }|rd t        |             dnd}|	d| | d| d| d| d
z  }	 |	sd}	d |       d t        |             d t         j                  dd                   d| d | d!| d"|	 d#S )$z@Render the per-run summary as branded HTML for the report email.dry_runr   u>  <div style="background:#fef3c7;border:1px solid #f59e0b;color:#92400e;padding:12px 16px;border-radius:8px;margin:0 0 16px;font-weight:600;">DRY RUN — no actual actions were taken. No Outlook drafts, ClickUp writebacks, or file uploads were performed. The counts below reflect what <em>would</em> have happened.</div>config_errorszConfiguration errors: ; errorzRun error: 	cancelledz$Run was cancelled before completing.z|<div style="background:#fee2e2;border:1px solid #ef4444;color:#991b1b;padding:12px 16px;border-radius:8px;margin:0 0 16px;">z<br>z</div>c           
   3   t   K   | ]/  \  }}d  |       dt        j                  |d             d 1 yw)zM<tr><td style="padding:6px 12px;border-bottom:1px solid #eee;color:#374151;">zo</td><td style="padding:6px 12px;border-bottom:1px solid #eee;text-align:right;font-weight:600;color:#111827;">r   
</td></tr>N)r   rU   ).0r   r9  escr  s      r   	<genexpr>z%_build_report_html.<locals>.<genexpr>  sP       C XX[\aXbWc d++.w{{3/B+C*DJ	Ps   58quotesr_   r  r   	recipientdraft_status	pdf_routez+<div style="color:#991b1b;font-size:12px;">z?<tr><td style="padding:6px 12px;border-bottom:1px solid #eee;">z@</td><td style="padding:6px 12px;border-bottom:1px solid #eee;">r  zh<tr><td colspan="4" style="padding:12px;color:#6b7280;text-align:center;">No quotes processed.</td></tr>z<div style="font-family:Arial,Helvetica,sans-serif;max-width:720px;margin:0 auto;color:#111827;">
  <h2 style="margin:0 0 4px;">Winston Pipeline Run Report</h2>
  <p style="margin:0 0 16px;color:#6b7280;font-size:14px;">
    Date: <strong>z-</strong> &nbsp;|&nbsp;
    Trigger: <strong>zI</strong> &nbsp;|&nbsp;
    Run ID: <span style="font-family:monospace;">r
   z</span>
  </p>
  z
  zt
  <h3 style="margin:16px 0 8px;">Summary</h3>
  <table style="border-collapse:collapse;width:100%;font-size:14px;">a  </table>
  <h3 style="margin:20px 0 8px;">Quotes</h3>
  <table style="border-collapse:collapse;width:100%;font-size:14px;">
    <tr style="background:#f9fafb;text-align:left;">
      <th style="padding:6px 12px;border-bottom:2px solid #e5e7eb;">Quote</th>
      <th style="padding:6px 12px;border-bottom:2px solid #e5e7eb;">Recipient</th>
      <th style="padding:6px 12px;border-bottom:2px solid #e5e7eb;">Draft</th>
      <th style="padding:6px 12px;border-bottom:2px solid #e5e7eb;">PDF route</th>
    </tr>
    z
  </table>
  <p style="margin:24px 0 0;color:#9ca3af;font-size:12px;">
    Automated report from Winston. Recipients are configured under
    Bots &rarr; Winston &rarr; Admin &rarr; Settings &rarr; Report Recipients.
  </p>
</div>)rv   rU   r   r   r   joinr   _REPORT_METRICS)r  re   r  r  bannernotes
notes_htmlmetric_rowsr  
quote_rowsqr   r  draftrouteerrerr_htmlr  s   `                @r   _build_report_htmlr    s   7;;y)*G
,,CF7 	 E{{?#-DIIgo>V4W0XXY{{7]SWW-=)>%??@{{;;<JEkk% !#+, 	 ''  *	 K [["(bFJ 
QUU>155%+@ABCAEE+r2;e<=	Cne456Ck5123eeGn B#c#h-PVW " 	MbTRZQ[ \JJS UJJO QJJOPZ\	


 K
 t9+ #g,'( )225c'++hPR:S6T2U1V W	( , FFQ] 	S L )
 
rs   c           	         t               }|j                  d      xs g D cg c]  }|s|	 }}|j                  d      xs g D cg c]  }|s|	 }}|s|st        j                  d       yt	        j
                         }|st        j                  d       yt        | j                  d            }d| |rdnd	z   }	t        | ||      }
t        j                  j                  d
d	      xs dj                         }t	        j                  ||	|
|||      }|r<t        j                  dt        |      t        |      || j                  d             |S t        j                  d| j                  d             |S c c}w c c}w )zEmail the run summary to the configured Report Recipients (DVI-976).

    Sends regardless of dry-run mode. Returns True if a message was accepted,
    False if there were no recipients or the send failed.
    	report_to	report_ccuA   Winston report: no Report Recipients configured — skipping sendFuA   Winston report: could not acquire Graph token — report not sentr  u   Winston pipeline report — z
 [DRY RUN]r   AZURE_MAIL_FROMztogen@icastinc.com)rf   cc_addressesz4Winston report sent to=%d cc=%d dry_run=%s run_id=%sr
   z$Winston report send failed run_id=%s)rL   rU   rE  rH  r   acquire_graph_tokenr  rv   r  osenvironr   	mail_sendr*  )r  re   r  rJ   eto_listcc_listr`   r  rY   	html_bodyrf   sents                r   _send_run_reportr  N  sh    H#<<4:AQqqAGA#<<4:AQqqAGA7TU--/E		UV7;;y)*G
&tf-"<	,  #7D':I zz~~/4L8LSSUG""w	7gD GWs7|Wgkk(6K	M K 			8'++h:OPK? BAs   F F FFr  c                     t        | ||||      }	 t        || |       |S # t        $ r) t        j	                  d|j                  d             Y |S w xY w)us  Run the pipeline and email the per-run summary report (DVI-976).

    Thin wrapper around :func:`_run_impl` that always attempts to send the
    summary report to the configured Report Recipients afterwards — including
    on dry runs and early-exit paths (config errors, no quotes, token failure).
    Report delivery never affects the returned summary or raises.
    )r
   r   r  z.Winston summary report send failed (run_id=%s)r
   )	_run_implr  r)  rE  rF  rU   )re   r  r
   r   r  r  s         r   runr  z  sc     gf$/BG-$0 N  -Fkk(+	-N-s   ! .AAc                 (   ddl m} t        j                  d| ||       t	               }t
        j                  j                  d      xs |j                  dd      j                         }|j                  dd      xs  t
        j                  j                  dd      j                         }|j                  d	d      xs  t
        j                  j                  d
d      j                         }	d}
|j                  dd      xs  t
        j                  j                  d|
      }t
        j                  j                  dd      j                         }|j                  dd      xs$ t
        j                  j                  dt              }|j                  d      xs t        j                  j                         }|j                  dd      xs t        j                  }|j                  d      xs	 t        d   }t        |      } |d|| |      }i d|j                  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d&g }|5  g }|s|j#                  d'       |s|	s|j#                  d(       |s|j#                  d)       |rM|D ]  }t        j%                  d*|        ||d+<   |j'                  d,j)                  |             |cd-d-d-       S g }|j+                  d.      5 }t-        |      }|r|j/                  || |/      }n|j1                  |	| |/      }i }|D ]&  }|j3                  |d0   g       j#                  |       ( g }|j5                         D ]  \  }}t7               }g }|D ]C  } | j                  d1      xs g D ])  }!|!|vs|j9                  |!       |j#                  |!       + E t;        d2 |D        |d         }"||"d1<   |j#                  |"        |}t=        |      |d<   t        j                  d3t=        |      |        dd4lm }# |D ]G  }$|$j                  d1      xs g }%|%D &cg c]
  }& |#|&       }'}&t        j                  d5|$d0   |%|'       I |jC                  |rd6nd7|xs d-|rd-n|	| |t=        |      d8|d9       	 |jE                  d:tG        jH                  |tJ        d;<      jM                  d=      d>|?       d-d-d-       |s2t        j                  dA|        |jS                  |       |cd-d-d-       S t        jT                         }(|(s6t        j%                  dB       dC|dD<   |j'                  dC       |cd-d-d-       S |D ]y  })tW        |j                        r8t        jY                  dE|j                         dF|dG<   |j[                           n,|)d0   }*|)dH   }+|*|+dI},d-}-|* dJ}.|j+                  dK|*       5 }/	 t]        |(|)|||      \  }0}1}2|0s-t        jY                  dL|*       dM|,dD<   |/j_                          n|0j                  dN|.      }.|0j                  dO      xs i }3|3j                  dP      xs d}4|4r|4 dQ|. n|2 dQ|. }5|/jC                  |.|1|2|5|0j                  dR      "ta        |0j                  dR      xs ddSz  dT      nd-|0j                  dU      |0j                  dV      dW       |0j                  dX      }6|6r5tc        j                  |6dYZ      }7|7jd                  d[k(  r|7jf                  nd-}-nK|1t        jh                  k(  rt        jj                  |(|2 dQ|.       }-nt        jl                  |(|1|2 dQ|.       }-|-r$t        j                  d\|.t=        |-      dSz         n+t        j%                  d]|.       d^|,dD<   |/j_                          d-d-d-       |-s|d&   j#                  |,       yg }9d-}:|j+                  da|*       5 };	 to        jp                  ts        jt                  |-      dFd;b      }<tw        |<|)|c      }=	 |jE                  ddtG        jH                  |=tJ        d;<      jM                  d=      d>|;?       ty        |<|-|)||*      }>|>|=j                  dfg       }?t        jY                  dg|*|)j                  d1      xs g |?|<jz                         dh|,dD<   |)j                  d1      xs g |?t}        |=j                  dig             dj|,dk<   |;jC                  d;|)j                  d1      xs g |?|=dl       |;j_                          	 |jE                  |.|-dm|;?       n|>do   }9|>dp   }:|:|,dp<   |9D @cg c]  }@|@dq   	 c}@|,dr<   |9D @cg c]  }@|@ds   	 c}@|,dt<   t        du |9D              dSz  |,dv<   |;jC                  dF|:|9D @cg c]  }@|@dq   |@ds   |@dp   |@dw   |@dx   dy c}@|9D @cg c]  }@|@dq   	 c}@|=dz       |9D ]D  }@t        j                  d{d||@ds   t        j                  }      }A|jE                  |A|@d~   d|;?       F d-d-d-       |9s|d&   j#                  |,       g }B|j+                  d|*       5 }C|9D ]  }@	 t        j                  ddt        j                  d{d@ds   t        j                  }            d|z   }Dt        j                  |(|@d~   |D      }E|Er4|jE                  @ds   EdC?       Bj#                  |@ds   |E|@dq   d       n*t        j%                  d|*@dq          Cj_                           	 d-d-d-       Bs(|,j3                  dDd       |d&   j#                  |,       t        d BD              dSz  |,d<   t=        |B      t=        |9      k  rt=        B       dQt=        |9       d|,d<   |j+                  d|*       5 }F	 |)j                  dd      }Gdj)                  d BD              }H|rXt        |)|:| |      }It        ||I      \  }J}Kd|,d<   Gxs d|,d<   |dxx   dTz  cc<   t        j                  d|*Gxs d|JH       nNt        |(||*      rd|,d<   |dxx   dTz  cc<   n-GrGg}Ln"|j                  dg       D Mcg c]  }M|MsM	 }L}Mt        |)|:| |      }It        ||I      \  }J}Nt        j                  |(|J|NL||j                  dg       D Mcg c]  }M|MsM	 c}M|j                  dg       D Mcg c]  }M|MsM	 c}MBD Ocg c]  }Ot        j                  |Od   |Od           c}O      }P|Pr=d|,d<   ||,d<   Gxs d|,d<   |dxx   dTz  cc<   t        j                  d|*Gxs d|       n"d|,d<   |dxx   dTz  cc<   Fj_                          d-d-d-       |j+                  d|*       5 }Q	 |xs |)j                  dd      }RBD Ocg c]  }O|Od   	 }S}Ot        |)|      \  }T}U|rRr|:dt        |:       dnd|,d<   ddj)                  S       d|,d<   UrKSD Vcg c]  }VUj                  dQ       dQ|V  }W}Vdt=        |W       dT ddj)                  |W       d|,d<   nd|,d<   |dxx   dTz  cc<   t        j                  d|*|,d   |,d   |,d          |d&   j#                  |,       	 d-d-d-       	_Rr=|:;j                  |+R|:      }X|Xrd|,d<   |dxx   dTz  cc<   n/d|,d<   |d xx   dTz  cc<   nRsdnd}Yd|Y d|,d<   |dxx   dTz  cc<   g }ZBD ]  }Oj                  |+|Od         r%Zj#                  Od    d       |d"xx   dTz  cc<   =|j                  |+Od   |Od         r%Zj#                  Od    d       |d!xx   dTz  cc<   {Zj#                  Od    d       |d#xx   dTz  cc<    d,j)                  Z      |,d<   Urg }[BD ]  }OUj                  dQ       dQ|Od    }\Tt        jh                  k(  rt        j                  |(\Od   d      }]nt        j                  |(T\Od   d      }]]r$[j#                  T d\        |d$xx   dTz  cc<   [j#                  Od    d       |d%xx   dTz  cc<    d,j)                  [      |,d<   nd|,d<   t        jY                  d|*       d-d-d-       |d&   j#                  |,       | |j                  d      s|j                  d%      r|j_                          |jS                  |d   |d   |d   |d   |d   |d   |d   |d    |d!   |d#   |d$   |d%   |d&   d       d-d-d-       t        j                  d| |d   |d   |d   |d   |d!   |d"   |d#   |d$   
       |S c c}&w # tN        $ r t        jQ                  d@       Y w xY w# 1 sw Y   xY w# tN        $ r9}8t        jQ                  d_|*       d`|8 |,dD<   |/j_                          Y d-}8~8
Pd-}8~8ww xY w# 1 sw Y   
ZxY w# tN        $ r t        jY                  de|*       Y 	w xY w# tN        $ r t        jY                  dn|*       Y 1w xY wc c}@w c c}@w c c}@w c c}@w # tN        $ rv}8t        j%                  d|*|8       d|8 |,dD<   |;j_                          	 |jE                  |.|-dm|;?       n%# tN        $ r t        jY                  dn|*       Y nw xY wY d-}8~8d-}8~8ww xY w# 1 sw Y   xY w# tN        $ rJ}8t        jQ                  d|*@dq          |,j3                  dDd|8        Cj_                          Y d-}8~8d-}8~8ww xY w# 1 sw Y   3xY wc c}Mw c c}Mw c c}Mw c c}Ow # tN        $ rC}8t        jQ                  d|*       d|,d<   |dxx   dTz  cc<   Fj_                          Y d-}8~83d-}8~8ww xY w# 1 sw Y   =xY wc c}Ow c c}Vw # tN        $ rF}8t        jQ                  d|*       |,j3                  dDd|8        Qj_                          Y d-}8~8vd-}8~8ww xY w# 1 sw Y   xY w# 1 sw Y   xY w)u  Process all quote tasks for the given date (YYYY-MM-DD).

    Returns a summary dict with aggregate counts and per-quote results.
    Designed to be called by the scheduler and the "Run now" UI endpoint.

    trigger: source of the run ("manual", "user", "scheduler:<schedule_id>").
    run_id: pre-generated UUID to use for the DiagnosticRun (so callers can
            return it to the UI before the background thread starts).
    pipeline_id: Date Logic pipeline id driving this run. Selects the
            per-pipeline email template override when one exists (Option C);
            falls back to the global template otherwise.
    dry_run: when True (DVI-908) the pipeline validates the full flow —
            ClickUp pull, workbook download, xlsx extract, xlsx→PDF convert,
            and diagnostics artifacts — but performs NO external writes: no
            Outlook draft, no ClickUp writeback, no Completed-Quotes/OneDrive
            upload. The generated PDF/xlsx are still saved as diagnostics
            artifacts (a temporary location) so the result is reviewable in
            Diagnostics without touching production systems.
    r   )DiagnosticRunu;   Winston pipeline starting — date=%s trigger=%s dry_run=%sCLICKUP_API_KEYr5   r   r6   CLICKUP_LIST_IDr7   CLICKUP_TEAM_IDdue_dater8   CLICKUP_DATE_FIELDCLICKUP_QUOTE_AMOUNT_FIELD_IDr:   WINSTON_TAKEOFFS_FOLDERr4   r9   r=   winston)botr  target_dater
   r
   re   r  r  r  r  r  r  r  clickup_amounts_skippedr  r  clickup_skippedr  r  r  r  zYmissing_clickup_api_key (set CLICKUP_API_KEY or clickup_api_key in winston_settings.json)zWmissing_clickup_source: set clickup_list_id or clickup_team_id in winston_settings.jsonz9missing_onedrive_user (set WINSTON_ONEDRIVE_USER env var)z!Winston pipeline config error: %sr  r  Nclickup_pull)
date_fieldr_   r   c              3   j   K   | ]+  }t         j                  |j                  d d            s| - yw)	task_namer   N)_SEND_QUOTES_REr+  rU   )r  _gts     r   r  z_run_impl.<locals>.<genexpr>  s2      MS+11#''+r2JK  Ms   13zClickUp pull: %d task(s) for %sr   z(ClickUp task %s tags_raw=%s tags_base=%sr   team)scopelist_idteam_idr  r  count)clickup_querytaskszclickup_tasks.jsonF)rq   ensure_asciizutf-8rC   )stepz-Failed to persist clickup_tasks.json artifactu$   No quote tasks found for %s — doneu*   Failed to acquire Graph token — abortinggraph_token_failedr  u-   Winston run %s cancelled by user — stoppingTr  clickup_task_id)r_   task_idz.xlsmzfind_download:zJWorkbook not found for quote %s in any configured Quote Take-offs locationworkbook_not_foundr   parentReferencepath/sizerm  r   lastModifiedDateTimewebUrl)	file_namera   folder	full_pathsize_kbmodifiedweb_urlz@microsoft.graph.downloadUrlx   )rw  r  zDownloaded %s (%d KB)zDownload failed for %sdownload_failedzDownload exception for quote %szdownload_exception: zopenpyxl_extract:rS  )r   r=   zworkbook_structure.jsonz.could not store workbook_structure.json for %sr'  uH   Quote %s: no quote sheet resolved — tags=%s candidates=%s available=%sno_quote_sheet_resolvedr  )r   r'  available_sheetssheet_resolution)r%  r   r'  workbook_structurexlsmz+could not store source xlsm artifact for %sr  r   r7  quote_sheetsrk  pdf_filenamesc              3   8   K   | ]  }t        |d            yw)rl  Nr*  )r  r.  s     r   r  z_run_impl.<locals>.<genexpr>  s     /UAlO0D/U   xlsx_kbr   r`  )r   pdfr   r   r`  )r%  r   r  r(  r  z\.pdf$z.xlsxr   rl  xlsxz"openpyxl extract failed for %s: %szopenpyxl_failed: zgraph_convert:z[^a-zA-Z0-9_-]rO   )scratch_filenamer  )filenamebytesr7  z'Graph PDF conversion failed for %s [%s]z)Graph convert exception for quote %s [%s]zpdf_conversion_exception: pdf_conversion_failedc              3   8   K   | ]  }t        |d            yw)r  Nr  r  ps     r   r  z_run_impl.<locals>.<genexpr>  s     "Aq3qz?"Ar  pdf_kbz sheets convertedpdf_partialzoutlook_draft:rc   z, c              3   &   K   | ]	  }|d      yw)r  Nrw   r  s     r   r  z_run_impl.<locals>.<genexpr>#  s     ,IqQz],Is   zdry_run (no draft created)r  z(no recipient)r  zP[DRY RUN] Would create draft  quote=%s  to=%s  mailbox=%s  subject=%r  attach=%sz(none)skippedr]   r[   r\   r  r  )rY   rZ   to_addressesrf   r  bcc_addressesattachmentscreatedz*Draft created  quote=%s  to=%s  mailbox=%sfailedz!Mail draft exception for quote %szclickup_writeback_route:quote_amount_field_idzdry_run (would set )zdry_run (skipped)clickup_amountzdry_run (would attach clickup_attachzdry_run (would upload z to z: r  z#dry_run (no destination configured)zI[DRY RUN] Would write back quote=%s: clickup_amount=%r attach=%r route=%rr   no_field_idno_grand_totalz	skipped (z	: skippedz
: attachedz: failedzapplication/pdf)content_type:z: upload_failedz#skipped (no destination configured)uf   No PDF route destination for %s — set shared_quotes_location in Togen Admin → Documents → Quotesz$ClickUp/route exception for quote %szwriteback_exception: )r  r  r  r  r  r  r  r  r  r  r  r  r  ud   Winston pipeline complete — date=%s  found=%d  drafts=+%d/~%d/!%d  attached=+%d/~%d/!%d  routed=%d)Qtogen.bot_diagnosticsr  rE  rH  rL   r  r  rU   r   _DEFAULT_TAKEOFFS_FOLDERr   WINSTON_MAIL_FROMWINSTON_ONEDRIVE_USERr>   r   idrv   r   r  failr  r  r   get_quote_tasksget_quote_tasks_by_team
setdefaultrm   r   r   nextr*  r   r   
set_detailadd_artifactrC   r~   r   encoder)  rF  set_summaryr  r   r{  cancelr   warnre  ry  rz  contentr   site_drive_read_fileuser_drive_read_filerW  rX  rY  rZ  r6  ro  r   r   rf  rQ   rR   r   xlsx_to_pdfr   r   r~  mail_create_draftencode_attachmentr  r   rstripset_custom_fieldattachment_existsattach_filesite_drive_upload_fileuser_drive_upload_file)^re   r  r
   r   r  r  rJ   r5   r6   r7   _default_date_fieldr  global_quote_amount_field_idr:   rf   r9   r=   email_templatediagr  errorsr  r  s1clickup_quote_groups_qt_deduped_qn_group
_seen_tags	_all_tagsr  _tag_primaryr   _t	_raw_tagstg
_base_tagsr`   r   r   r  r/  rP  	xlsm_names2r   _src_upn_src_folder_parent_ppath
_full_pathdl_urldl_respexcr  r   s3r   	structureextractr  r.  xlsx_artifactpdfss4scratch_name	pdf_bytess5r  attach_namesctxrY   _bodyr  arZ   r  r  s67field_id	pdf_namesdest_upndest_folderr   
dest_pathsokreasonattach_statesroute_states	dest_pathuploadeds^                                                                                                 r   r  r    s   * 4HHJ7G%H 	

()PX\\:KR-Peg  	&+Trzz~~>OQS/Teg  	&+Trzz~~>OQS/Teg  %)2. 	E::>>.0CD  $&::>>2QSU#V#\#\#^ &+ 	O::>>35MN  ||O,O0O0OVVXGLL"5\9\9\M ll9%E):9)EG )5NYTRXYD$'' 	4= 	!!	
 	 	! 	! 	 	q 	"1 	!! 	A 	1 	! 	a  	A!" 	"#G( 
 sMMuvMMstMMUV B		=qAB'-GO$IIdii'(s s& YY~& D	O"#O4G//R\/]  77Zd7e .0M N((^)<bAHHMN#%H,224 *V'*u
')	! 3C!$!6B 3z1&NN40%,,T233  MF M1I
 $- )!*" E&)%jGN#HH6E
DI
 B FF6N0b	AJK2226K
K>~&	: MM'6VF.6$'6tO#'", Z" 
 
O!!(JJucFMMgV " }D	OL HH;TBW%{s s@ 113IIBC3GGII*+Ks sN  x	-DDGG$KTWWU'+$n%B,-G,.7CF
 (,J$eI^B401 4R32HtX}o3/D(K  78: +?w	$(HHVY$?	 #'((+<"="C!(V!4!:AG)&=.9]!I;+G #)2&.&1)3+/88F+;+G ).txx/?/D1.La(PMQ(,1G(H'+xx'9	' 	 "&*H!I!&*hhvs&CG<C<O<OSV<V\`J%)E)EE)6)K)K %+a	{'C*J *7)K)K %xK=)1M*J &HH%<iZ\`I`aII&>	J.?F7OGGIa4l !((0 "$F"&K.rd34 PWOW "//

:.$B !:"4QX YIZ))5 JJy#ERYYZab" *  5RT7TVWG &/]]3Er%J
f 0 6B
BMM +Dw$(HHV$4$:0:04Y]]=RT5U0V612
 ',$(HHV$4$:0:2;	'  	[ --iVRT-U ")!2&-m&<0;}-KQ1Ra!L/1R~.NT2U1^3D2U/,//Uf/U,UY],]y)'++6 *0	' %& ./|_+,^+<34]3C89:L8M,-fI!"	' JP/PA,/P2;'   "( _A,.FF9gqGX`b`m`m,nM --mQ|_f[]-^_IPWd !((0  "D^B401 "R "A" FF#4c#%66)R>9JRTR_R_#`b%& %
 %2$=$=!1\?\%	 % --a.?EXZ-[ KK,-n,=)2./o)   II&O&(!L/;GGI+"": !!'+BC!((0""AD"AATIF84y3v;&+.t9+Qs6{mCT(U}%
 ^B401 <R; $):B ?I $(99,ID,I#IL 2$T7S)5nc)J1M~..7.K;K{+ 89Q>9n	 5Xw 'ugr:1:~. 01Q61 %'0kG2@2D2D]TV2W&]Q[\q&]G&]1$T7S-9.#-N* - ? ?!$+&/)0$+5C5G5Gb5Q)WUV!)W6D6H6HPR6S*YWX1*Y *.)$% !. ? ?*qQXz Z)! !5>F>26=F?32;2O?OF;/#$45:5HH L "I$97
 6>F>2#O494GGIo<B 5bT:; ^s];dtxxH_ac?dH 9= =1: =I =,FtX,V)Hk  (K,C 2-2L1MQO!4 /0 5TYYy5I4J!L /0 'T])^q[-?-?-D,EQqc*J)^J)^"8Z8I#+*Btyy/D.EQ!H #;/
 3XF;/ 89Q>9g'7 8&AQ:RTZ[fTg  )008 A^ ^F  K$;$55gxU7<F#34#$9:a?:7?F#34#$<=B=6>DT5>vha3H/0 9:a?: 02M! 	;"44Wa
mL)00AjM?)1LM#$56!;6$00!J-7T)00AjM?*1MN#$671<7)00AjM?(1KL#$45:5	; 04yy/GF+, #24!% AA+6+=+=c+B*C1Qz]O(TI'=+G+GG+8+O+O$)9aj1B,"
 ,9+O+O$)8Y'
1B,"  ( , 3 3xj)4M N ' 5 : 5 , 3 3q}o_4U V '(: ;q @ ;#A$ /3ii.E{+.S{+am^@ H$$V,qx	-v ;;'7;;7I+JIIKy)&-.F&G#N3%&67%&67$_5#*+@#A&-.F&G '(: ;%&67!,/ '(: ;h'
 	Ksj HH	, ! ! "#!" ! NC L,  OMNOGD	O D	Of ! MM"CRH(<SE&BF7OGGIIc4 4d % Z$TVXYZ8  ) [KK(UWYZ[ 2S2U
	' 0Q ! 	WIIBBL(9#&?F7OGGIW)))Zb)Q$ W$QSUVW	WOPW PWh % "&Q&(!L/;))'5OPSu3UV			"/" "F '^ *X*Y)$ ! MM"ErJ-5F>*O,1,GGII	q< <N !> *_P ! MM"H"M%%g1Fse/LMHHJJw^ ^s ssl  BAQ)AQ<B)AG&B(AGAF*
AAG->AF/+2AQ'AAQ3A=AQ0AH&2GAG!8AQAL9AJ>AH3 CAJAI"AJ6AI?

AJAJ
/AJAJ	%AJ+AJ7AAJ
6AQ AM8B=AL"AM8BAQAO(B?ANAN
"AN
&AAN-AN
5AN
9ANANANAN!#ANA)AN-AQ	AQAO?%AO5
1A	AO?:AO:
A2AO?

AQG?AO?CBAQF*AGF/AGGAGGAGGAGGAG	GAQG!	AH#	G*.AH	HAH&HAH#	H#AH&H&AH0H+AQH3AI	IAJIAI	IAJIAI<	I8AJI;AI<	I<AJJ	AL	J/AL	KAK"K!AL	K"ALLAL	LALLAL	LALLAL	LALLALLAQL"	AM5L+?AM0M*AM8M0AM5M5AM8M8ANM=AQNANN	AO%	N"8AO 	OAO(O AO%	O%AO(O(AO2O-AQO5
AO?O?	AQ	P;AQ		QAQQ	AQ	QAQQAQQAQQAQ(__main__z$%(levelname)s  %(name)s  %(message)s)levelformatz Winston quote pipeline (DVI-815))descriptionz--dateTzTarget date YYYY-MM-DD)requiredhelprz   )r|   rq   r  r  r   r   )manualNNF)o__doc__htmlr   rY  rC   loggingr  rQ   	threadingr   r   pathlibr   rW  requestsry  openpyxl.utilsr   togenr   r   r   	getLoggerrE  Lockr   r   r	   r   __annotations__r   rv   r   r   __file__resolveparentrA   rT   ri   compiler  r   r   r  r  r>   r@   rL   rX   r^   rg   r   rj   rr   ru   r   r   r   r   r   objectr   r   r   tupler   r   r   r   r   r   r   r   r   r   r  r   r6  r:  r=  rM  r  rb  rh  ro  r~  r  r  r  r  r  r  __name__argparse_jsonsysbasicConfigINFOArgumentParserapadd_argument
parse_argsargsre   r/  printr~   rU   	has_errorexitrw   rs   r   <module>r     s)  'R  	   	 	  '    ,  .g01 y~~!e 3s8 #&3 &4 &+ + +
*# *$ * H~--/669PPH~--/66H H~--/669VV  "**89 0  D  * $ %' !xhdKX# 4 0	 		3 	4 	6 ?	? !    +de  ,J  K+VW+[\+RS+ded4j 2	4 	D T Ad A
Ad At A4s 4t 4It IS4Z 4 (AT Ad
 Ad ABBv B# B	d 	 	s 	S 	UY 	 AF 3# 3 3 3# 3"4 $ 5c? 	d 	&&&&*&;>&QT&
=#s"#&h1s 1s 1
C 
% 
% 
%C $ 49 0$  $uS#X:O U6<#78  4 $ 4: @8NT 8Nx "&b#pp0=pp(+p<?p 
pf	3 	3 	FS Fc F3 F3 F-3 -3 -h ,0	9 9 9  )	9 
 5$;9 x1T
 1~ 1(::!%:04:AD::@   s  #  $  L5d 5d 5uS#X 5BS
 S
C S
# S
# S
l&d &# & & &X BF8=c C C$J 4Z15BF& HL>CU	C U	# U	#* U	:U	7;U	HLU	v zGgll3YZ	 	 	 -O	PBOOHt2JOK==?D^F	+%++fQ
45VZZ(GFJJ,GHICHH)Q# rs   