#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import json, os, re, sys, time, datetime, urllib.request, urllib.parse

BASE = os.path.expanduser("~/Claude/Projects/에이전트구축")
MEDIA = os.path.join(BASE, "media_feed.jsonl")
XFEED = os.path.join(BASE, "x_feed.jsonl")
STATE = os.path.join(BASE, "sent_alerts.json")
REJECT = os.path.join(BASE, "rejected_feed.jsonl")
NQLEV = os.path.join(BASE, "nq_levels.py")

CUT = 5          # 미디어
CUT_X = 7        # X피드 (션이 직접 봄)
DRY = "--dry" in sys.argv
HOURS = 2
if "--hours" in sys.argv:
    HOURS = int(sys.argv[sys.argv.index("--hours") + 1])

def creds():
    tok = os.environ.get("BOT_TOKEN"); cid = os.environ.get("CHAT_ID")
    if tok and cid: return tok, cid
    try:
        s = open(NQLEV, encoding="utf-8").read()
        tok = tok or re.search(r'BOT_TOKEN\s*=\s*["\']([^"\']+)', s).group(1)
        cid = cid or re.search(r'CHAT_ID\s*=\s*["\']?([^"\'\s]+)', s).group(1)
    except Exception:
        return None, None
    return tok, cid

def send(msg):
    tok, cid = creds()
    if not tok or not cid:
        print("[!] BOT_TOKEN/CHAT_ID 못 찾음"); return False
    data = urllib.parse.urlencode({
        "chat_id": cid, "text": msg,
        "disable_web_page_preview": "true"}).encode()
    try:
        urllib.request.urlopen(
            f"https://api.telegram.org/bot{tok}/sendMessage", data=data, timeout=15)
        return True
    except Exception as e:
        print("[!] 전송 실패:", e); return False

UNIV = ["MU","SNDK","NBIS","AAOI","SOXL","DRAM","META","NVDA","LITE","TER","ALAB",
        "COHR","MRVL","CRDO","GLW","AVGO","CIEN","AEHR","ASML","LRCX","AMAT","KLAC",
        "SKHY","AMD","INTC","TSM","MSFT","GOOGL","AMZN","ORCL","IBM","NOW","CRM",
        "ACN","JPM","CRWV","IREN","QQQ","NDX","AXTI"]
UNIV_KO = ["하이닉스","삼성전자","마이크론","엔비디아","샌디스크","키옥시아","메타","브로드컴"]

PRIMARY_SRC = {"reuters","bloomberg.com","bloomberg","wsj","cnbc","the information",
               "eetimes.com","semiengineering.com","연합인포맥스","전자신문","dart",
               "the korea economic daily global edition","asml","nasdaq",
               "trendforce","counterpoint research","counterpoint",
               "semianalysis","semianalysis.com","digitimes","financial times"}
PRIMARY_X = {"@DeItaone","@SemiAnalysis_","@jukan05","@KobeissiLetter","@ValleyAI_X",
             "@DrNHJ","@pequityresearch","@stocktalkweekly","@Barchart"}


def _srckey(src, url=""):
    a = str(src or "").lower().strip()
    h = ""
    try:
        from urllib.parse import urlparse
        h = (urlparse(url).hostname or "").lower().replace("www.", "")
    except Exception:
        pass
    return a + " | " + h

BLOCK_TOK = {"motley fool","fool.com","seeking alpha","seekingalpha","simplywall",
             "tipranks","marketbeat","quiver","barchart","24/7 wall","moomoo",
             "benzinga","yahoo finance","finance.yahoo","yahoo.com","tradingview",
             "coindesk","bitget","crypto briefing","pluang","kdnuggets","sammobile",
             "globe and mail","moneycontrol","times of india","eciks",
             "investing.com","zacks","insider monkey","nate.com","네이트","daum.net"}

def blocked(src, url=""):
    k = _srckey(src, url)
    return any(tok in k for tok in BLOCK_TOK)

BLOCK_SRC = {"the motley fool","seeking alpha","simplywall.st","tipranks","marketbeat",
             "quiver quantitative","barchart.com","24/7 wall st.","moomoo","benzinga",
             "yahoo finance","yahoo finance uk","yahoo finance singapore","tradingview",
             "coindesk","bitget","crypto briefing","pluang","kdnuggets","sammobile",
             "the globe and mail","moneycontrol.com","the times of india","eciks.org",
             "investing.com","zacks","insider monkey","nate","네이트","v.daum.net"}
DEMOTE_SRC = {"tomshardware.com","the globe and mail","business insider"}
BLOCK_X = {"@1ChartMaster","@AshCrypto","@RosannaInvests","@MMatters22596","@saso_capital",
           "@michaelsikand","@CaesarCapitalz","@WhaleTwits","@TradexWhisperer",
           "@aleabitoreddit","@ParadisLabs","@nandeyare","@pelositracker"}

CUT_PAT = re.compile(
    r"(왜 오늘|오늘 왜|why is .*(stock )?(up|down|surging|rising|falling)|"
    r"here is why|what you need to know|outpaces stock market|"
    r"better buy|which .*is the better|where will .* be in 20|"
    r"mind-boggling|is (it )?a buy|buy, sell or hold|looks below fair value|"
    r"stocks that explain|biggest analyst calls|"
    r"price target (raised|lowered|cut) to|목표주가 (상향|하향)만|"
    r"best (stocks|picks)|top \d+ stocks|"
    r"review:|deals|newegg|ssd review|emulation|drone|gaming|laptop|"
    r"청약|최저임금|맛집|음악|뮤직|유튜브|밈|구독자|팔로워)", re.I)

KR_CUT = re.compile(
    r"(외국인|기관) *(순)?(매수|매도)|신용잔고|사이드카|서킷브레이커|"
    r"코스피 *지수|코스닥|금통위|한국은행|기준금리 *(인상|인하)|이재명|"
    r"국장|반대매매|공매도|배당소득세|상법|밸류업|레버리지 *ETF *(논란|규제)|"
    r"청약|부동산|주택|전세|최저임금", re.I)

SUPPLY_CUT = re.compile(
    r"(data ?cent(er|re).*(moratorium|permit|ban|freeze|zoning|tax break)|"
    r"모라토리엄|인허가 *동결|전력망|송전|발전소 *건설|가스 *터빈|"
    r"interconnection queue|power purchase|environmental|배출|오염)", re.I)

NUMCHG = re.compile(
    r"(컨센|예상치|전망치|시장 *예상|가이던스|guidance|vs\.? *(consensus|est)|"
    r"estimates?|상회|하회|상향|하향|beat[s]?|miss(ed|es)?|raise[ds]? .*(to|target)|"
    r"cut to|revis(ed|es)|QoQ|YoY|전년 *(동기)? *대비|전분기 *대비|"
    r"기록|사상 *최|record)", re.I)
NUMANY = re.compile(
    r"[+\-]?\d[\d,\.]* *(%|퍼센트|억|조|억달러|billion|million|trillion|bps|만개|대)", re.I)

EARN = re.compile(
    r"(어닝콜|실적 *발표|컨퍼런스 *콜|conference call|earnings call|"
    r"분기 *실적|quarterly results|Q[1-4] *(실적|results|earnings)|"
    r"가이던스|guidance|IR *공시|기업설명회|preliminary results|예비 *실적)", re.I)

TECH_GATE = re.compile(
    r"(HBM4?|HBF|CXMT|YMTC|루빈|Rubin|CoWoS|COUPE|CPO|실리콘 *포토닉스|"
    r"silicon photonic|KV *cache|KV *캐시|양자화|quantization|MoE|"
    r"인터포저|interposer|EUV|DUV|WFE|수율|yield|테이프아웃|tape ?out|"
    r"1\.6T|800G|DDR5|LPDDR|스택|stack|베이스 *다이|base die|번인|burn-?in)", re.I)

THESIS_BREAK = re.compile(
    r"(capex *(cut|guidance *cut|reduction|삭감|축소)|자본지출 *(축소|삭감)|"
    r"inference *(cost|memory) *(reduction|efficienc\w*|saving\w*|절감|효율|down|하락)|"
    r"추론 *(비용|메모리) *(절감|효율|하락)|"
    r"CXMT.*(DDR5|DRAM|양산|출하|capacity|캐파|점유율|matching|근접)|메모리 *(수요 *둔화|가격 *하락)|"
    r"(DeepSeek|Qwen|Kimi|Moonshot|MiniMax|Zhipu|GLM-?\d|Ernie|Doubao|디프시크)"
    r"[^\n]{0,80}(cheap|low-?cost|cost|pricing|price|가격|저가|free|open[- ]?source|오픈소스|"
    r"efficien\w*|효율|beat\w*|surpass\w*|outperform\w*|close[sd]? the gap|격차|"
    r"release\w*|launch\w*|unveil\w*|debut\w*|preview|V\d|출시|공개)|"
    r"(Chinese|China'?s?|중국)[^\n]{0,40}(A\.?I\.?|AI)?[^\n]{0,20}(model|lab|모델)s?"
    r"[^\n]{0,70}(cheap|low-?cost|cost|adopt\w*|switch\w*|shift\w*|gaining ground|surge|"
    r"close[sd]? the gap|catch\w* up|가격|저가|채택|전환|확산|잠식|격차)|"
    r"(Chinese|China'?s?|중국)[^\n]{0,30}(affordable|cheap|low-?cost|저가)"
    r"[^\n]{0,30}(A\.?I\.?|AI|모델))", re.I)

# 축A 수요축 — 티커 없이 오는 추론수요 실측치 (숫자 동반 시 게이트 통과)
DEMAND_AXIS = re.compile(
    r"(token[s]?[^\n]{0,25}(per (minute|second|day|month)|daily|usage|consumption|"
    r"demand|growth|processed|processes)|"
    r"\d[\d,\.]* *(billion|trillion|million|조|억) *tokens?|"
    r"토큰[^\n]{0,20}(사용량|소비|수요|처리|증가)|"
    r"inference (demand|volume|workload|spending)|추론 (수요|물량|지출)|"
    r"(price|pricing|cost)[^\n]{0,25}per million tokens|per million tokens|"
    r"100만 토큰|per-token (price|cost))", re.I)

# 축C 최종재 수요 파괴 — 메모리 논지 정면 반증
END_DEMAND = re.compile(
    r"((notebook|laptop|PC|smartphone|handset|노트북|스마트폰|휴대폰)[^\n]{0,50}"
    r"(shipment|출하|demand|수요)[^\n]{0,40}(cut|lower|decline|revised down|하향|감소|둔화|축소)|"
    r"(cut|lower|revis\w+|slash\w*|하향|축소)[^\n]{0,45}"
    r"(notebook|laptop|PC|smartphone|handset|노트북|스마트폰)[^\n]{0,35}(shipment|forecast|출하|전망)|"
    r"(memory|DRAM|LPDDR|메모리)[^\n]{0,70}(bill of materials|BOM|원가 비중)|"
    r"(smartphone|handset|notebook|laptop|PC|스마트폰|노트북)[^\n]{0,40}"
    r"(raise[sd]?|hike[sd]?|increase[sd]?|인상)[^\n]{0,20}price[^\n]{0,45}"
    r"(memory|LPDRAM|LPDDR|DRAM|메모리))", re.I)

# CXMT 실물화 트리거 (2026-07-17) — 티어1이 실제 채택하면 논지파괴
T1 = r"(Apple|Dell|HP|HPE|Lenovo|Asus|Acer|Microsoft|Google|Amazon|Meta|애플|델|레노버)"
CN_MEM = r"(CXMT|ChangXin|YMTC|Chinese (DRAM|memory|NAND)|중국산? *(D램|DRAM|메모리|낸드))"
ACT = (r"(qualif\w*|adopt\w*|sourc\w*|purchas\w*|order\w*|test\w*|approv\w*|"
       r"design win|채택|승인|구매|도입|테스트|검증)")
TIER1_CXMT = re.compile(
    "(" + T1 + r"[^\n]{0,60}" + CN_MEM + r"[^\n]{0,60}" + ACT + "|"
        + T1 + r"[^\n]{0,40}" + ACT + r"[^\n]{0,60}" + CN_MEM + "|"
        + CN_MEM + r"[^\n]{0,60}" + ACT + r"[^\n]{0,40}" + T1 + ")", re.I)

# 전주(하이퍼스케일러 5사) 지갑 축 (2026-07-17) — FCF·조달방식이 CapEx 가이던스보다 빠름
PATRON = r"(Meta|Alphabet|Google|Amazon|Microsoft|Oracle|하이퍼스케일러|빅테크|메타|알파벳|구글|아마존|마이크로소프트|오라클)"
FIN_EVENT = (r"(negative free cash ?flow|free cash ?flow[^\n]{0,25}(negative|decline|drop|fall|plunge|below zero)|"
             r"FCF[^\n]{0,20}(negative|마이너스)|잉여현금흐름[^\n]{0,15}(마이너스|급감|감소)|"
             r"(bond|note|debt)[^\n]{0,15}(sale|offering|issuance|raise)|채권[^\n]{0,10}발행|회사채[^\n]{0,10}발행|"
             r"(pause[sd]?|halt\w*|suspend\w*|중단)[^\n]{0,20}(buyback|repurchase|자사주)|"
             r"(buyback|repurchase|자사주)[^\n]{0,20}(pause|halt|suspend|중단)|"
             r"(SPV|special purpose vehicle|private credit|off-?balance)[^\n]{0,30}(data ?cent|financ|capex)|"
             r"capex[^\n]{0,25}(exceed|outstrip|surpass)[^\n]{0,25}(cash ?flow|operating cash)|"
             r"(cash ?flow|operating cash)[^\n]{0,25}(crossover|교차))")
PATRON_FIN = re.compile("(" + PATRON + r"[^\n]{0,70}" + FIN_EVENT + "|"
                            + FIN_EVENT + r"[^\n]{0,70}" + PATRON + ")", re.I)

STACK_BOTH = re.compile(
    r"((메타|Meta|Microsoft|Google|Amazon|OpenAI|xAI|Oracle)[^\n]{0,60}"
    r"(capex|자본지출|투자|investment|buildout|증설|확장|expansion|GW|기가와트)|"
    r"(self-build|자체 *(구축|증설)|in-house (cloud|compute)|own (data ?cent|cloud)))", re.I)

# 차노스축 (2026-07-18) — 감가상각 시한폭탄 / 증분ROIC 하락 / 미들맨 디레이트
# 전주지갑축(조달방식)과 별개: 이쪽은 회계·수익성 신호(실적 콜에서 나오는 단어)
CHANOS_ACC = re.compile(
    r"(depreciation[^\n]{0,30}(revenue|sales|earnings|net income|이익|매출)|"
    r"(감가상각|상각비)[^\n]{0,20}(매출|이익|비율|급증|증가)|"
    r"(construction[- ]in[- ]progress|CIP|건설중공사|재공품)[^\n]{0,40}(depreciat|transfer|placed in service|고정자산 *전환|상각)|"
    r"(useful life|useful lives|내용연수|depreciation schedule|상각 *(기간|연수))[^\n]{0,40}"
    r"(understate|overstate|mismatch|extend|불일치|과소|과대|연장|늘려)|"
    r"(useful life|내용연수|상각)[^\n]{0,30}(2-3|two to three|3-?year|5-?6|five to six)[^\n]{0,15}(year|년)|"
    r"(incremental|증분)[^\n]{0,10}(ROIC|ROI|return on (invested )?capital|투하자본|자본수익)|"
    r"(ROIC|return on (invested )?capital)[^\n]{0,25}(decline|drop|fall|compress|하락|둔화|악화|40%.{0,10}20%|20%.{0,10}10%)|"
    r"(write-?down|writedown|impair\w*|상각손실|손상차손)[^\n]{0,30}(GPU|data ?cent|AI|accelerat|chip|서버|데이터센터)|"
    r"(middleman|middlemen|중간자|중간상|equipment leas\w*|장비 *리스)[^\n]{0,40}"
    r"(re-?rat\w*|multiple|배수|디레이트|밸류에이션|valuation))",
    re.I)

# AI모델 발표 감시 (2026-07-19) — 크기 아닌 "방향"으로 판정
# 대상: 중국 오픈웨이트 러시 전체 (kimi/qwen/deepseek/GLM/z.ai/minimax 등)
AI_MODEL_NAME = re.compile(
    r"(kimi|moonshot|qwen|通义|deepseek|深度求索|GLM|z\.?ai|zhipu|智谱|minimax|"
    r"ernie|文心|doubao|豆包|hunyuan|混元|step-?\d|阶跃|llama\s?\d|mistral|"
    r"gemini\s?[\d.]+|gpt-?[\d.]+|grok\s?\d|open-?weight|오픈웨이트)", re.I)

# 효율 축 = 메모리 논지파괴 = 시장충격 (같은성능에 자원 덜 씀)
AI_MODEL_EFF = re.compile(
    r"(active param\w*|활성 ?파라미터|per-?token cost|토큰당 ?비용|inference cost[^\n]{0,20}(down|drop|cut|하락|절감)|"
    r"추론 ?비용[^\n]{0,10}(하락|절감|급감)|linear attention|선형 ?어텐션|KV[- ]?cache[^\n]{0,15}(reduc|down|축소|줄)|"
    r"(90%|절반|half|10%)[^\n]{0,15}(cheaper|저렴|cost|비용)|distill\w*|증류|"
    r"efficiency breakthrough|효율[^\n]{0,5}(혁신|돌파)|runs on[^\n]{0,20}(consumer|single|1 ?GPU|소비자))", re.I)

# 확산 축 = 메모리 논지강화 = 안도 (대형 MoE, 토큰폭증, 제본스)
AI_MODEL_AMP = re.compile(
    r"(trillion param\w*|조 ?파라미터|\d\.?\d?T param|MoE|mixture of experts|"
    r"token (usage|demand)[^\n]{0,15}(surg|explod|증가|폭증|확대)|토큰 ?(사용|수요)[^\n]{0,10}(증가|폭증)|"
    r"jevons|제본스|HBM[^\n]{0,15}(more|늘|증가|수요)|network bottleneck|네트워크 ?병목|"
    r"scale-?up[^\n]{0,15}(network|더|강))", re.I)

# 발표 예정 (뒷북 방지 — 임박 감지)
AI_MODEL_SOON = re.compile(
    r"(deepseek|kimi|qwen|GLM|문샷|moonshot)[^\n]{0,40}(release|launch|unveil|공개|출시|발표)[^\n]{0,20}"
    r"(soon|imminent|days|this week|내|곧|임박|예정|주 ?내)|"
    r"(expected|anticipat|예상)[^\n]{0,30}(deepseek v4|kimi k|qwen \d|GLM \d))", re.I)

# 메모리 공급완화 논쟁 (2026-07-19) — 매크로 나쁠 때 기관이 쥐는 베어 명분
# "2028 증설 완료→부족 완화"는 션 롱 논지의 반대축. 뜨면 반드시 양면 봐야 함.
SUPPLY_EASE = re.compile(
    r"((2028|2027)[^\n]{0,40}(oversupply|과잉공급|glut|ease|완화|normaliz|풀린|해소|inflection|변곡)|"
    r"(oversupply|과잉공급|공급 ?과잉|glut|supply glut)[^\n]{0,30}(risk|우려|리스크|2027|2028)|"
    r"(new fab|신규 ?(팹|라인)|증설)[^\n]{0,40}(volume|물량|완화|ship|2028|과잉)|"
    r"(memory|메모리|dram)[^\n]{0,20}(shortage|부족)[^\n]{0,20}(ease|완화|end|끝|해소|2028)|"
    r"(초과공급|공급 ?정상화|가격 ?정상화|cyclical[^\n]{0,15}(risk|peak|top))|"
    r"(downgrade|하향|initiate hold|홀드 ?개시|sell rating)[^\n]{0,25}(dram|memory|메모리|micron|hynix|삼성))",
    re.I)

# ===== 축 신설 (2026-07-20): capex유지·상향 / 공급자 마진억제 / 광학전환 =====
# 지배질문 트래킹: (GPU) 컴퓨팅부족 강화 / (메모리) 마진 / (광학) 시장확대 촉매
CAPEX_UP = re.compile(
    r"((capex|자본지출|투자액|capital expenditure|capex guidance)[^\n]{0,45}"
    r"(raise[sd]?|hike[sd]?|increase[sd]?|boost\w*|lift\w*|reaffirm\w*|maintain\w*|"
    r"reiterat\w*|상향|확대|증액|유지|재확인)|"
    r"(raise[sd]?|hike[sd]?|boost\w*|상향|확대|증액)[^\n]{0,25}"
    r"(capex|자본지출|투자 ?계획|buildout|증설)|"
    r"(big ?four|빅4|하이퍼스케일러|hyperscaler)[^\n]{0,45}(capex|자본지출)[^\n]{0,25}"
    r"(\d|billion|억|조|%))", re.I)

# 공급자 자발적 마진억제 = 메모리 "부족해도 안 폭등"의 원인
SUPPLIER_MARGIN = re.compile(
    r"((price cap|price ceiling|가격 ?상한|장기계약[^\n]{0,15}(고정|상한)|"
    r"(long-?term|multi-?year) (agreement|contract)[^\n]{0,20}(fix\w*|lock\w*|ceiling|cap)|"
    r"\bSCA\b|\bLTA\b)[^\n]{0,30}(price|가격|memory|dram|메모리)?|"
    r"(margin|마진|GPM|gross margin|매출총이익률)[^\n]{0,30}"
    r"(too high|abnormal\w*|비정상|과도|너무 ?높|낮춰|lower|reduce|dilut\w*|희석|compress\w*|압박)|"
    r"(마진|margin)[^\n]{0,12}(낮춰야|내려야|공멸|공생)|"
    r"(감산|production cut|reduce output|capacity discipline|공급 ?조절)[^\n]{0,25}"
    r"(memory|dram|nand|메모리|낸드)|"
    r"(not ?rais\w*|가격[^\n]{0,8}(억제|자제|안 ?올)|price restraint)[^\n]{0,25}"
    r"(memory|dram|chip|메모리))", re.I)

# 광학전환 촉매 = 주도플레이어(엔비디아·AVGO·마블·AMD)가 광학채택 규모를 밝히는 것
OPTICS_LEAD  = r"(Nvidia|엔비디아|NVDA|Broadcom|브로드컴|AVGO|Marvell|마블|MRVL|AMD|Rubin|루빈|hyperscaler|하이퍼스케일러)"
OPTICS_TECH  = r"(CPO|co-?packaged optics|공동 ?패키징|NPO|near-?package|silicon photonic\w*|실리콘 ?포토닉스|optical (I/O|interconnect|engine)|광 ?(연결|엔진|입출력))"
OPTICS_ADOPT = r"(adopt\w*|standard\w*|roadmap|integrat\w*|ship\w*|volume|production|채택|편입|표준|양산|로드맵|탑재|규모|attach ?rate|penetrat\w*|침투)"
OPTICS_TRANSITION = re.compile(
    "(" + OPTICS_LEAD + r"[^\n]{0,60}" + OPTICS_TECH + r"[^\n]{0,50}" + OPTICS_ADOPT + "|"
        + OPTICS_TECH + r"[^\n]{0,50}" + OPTICS_ADOPT + r"[^\n]{0,40}" + OPTICS_LEAD + "|"
    r"(copper|구리|electrical|DAC|ACC)[^\n]{0,25}(to|→|transition|shift|replac\w*|전환|대체)[^\n]{0,25}(optical|photonic|광)|"
    r"(InP|indium phosphide|인듐 ?인화물|AXTI)[^\n]{0,40}(demand|order|수요|수주|출하|shortage))", re.I)

OPINION_ONLY = re.compile(
    r"(analyst|애널리스트|목표주가|price target|투자의견|rating|"
    r"initiat(e|ed) coverage|reiterat|maintains?|overweight|비중확대)", re.I)

IRAN = re.compile(
    r"(이란|Iran|호르무즈|Hormuz|IRGC|혁명수비대|테헤란|Tehran|"
    r"페르시아만|Strait of Hormuz|핵 *(시설|합의)|nuclear (site|deal))", re.I)

KRREG = re.compile(
    r"((해외|무허가|미등록|불법) *(가상자산)? *거래소|특금법|가상자산 *이용자 *보호법|"
    r"금감원.*(경고|제재|차단|점검)|금융위.*(해외|거래소|레버리지)|"
    r"(바이빗|바이낸스|Bybit|Binance|OKX).*(규제|차단|경고|제재|조사|접속)|"
    r"레버리지 *(ETF|상품).*(규제|제한|승인|논란)|"
    r"주식 *(퍼페추얼|무기한|선물).*(규제|금지)|해외 *선물.*(규제|과세))", re.I)

TRUMP = re.compile(r"(트럼프|Trump|TRUMP:|백악관|White House)", re.I)
TRUMP_NQ = re.compile(
    r"(관세|tariff|연준|Fed|금리|rate|반도체|chip|semiconductor|엔비디아|Nvidia|"
    r"수출 *(규제|통제|허가)|export (control|ban|license)|AI|인공지능|"
    r"빅테크|big tech|증시|stock market|나스닥|Nasdaq|감세|tax cut|"
    r"애플|Apple|메타|Meta|구글|Google|아마존|Amazon|마이크로소프트|Microsoft|"
    r"H200|H20|블랙웰|Blackwell|中|중국|China)", re.I)

WIDGET = re.compile(
    r"[A-Z][A-Za-z\.\-&' ]{1,28}(Corp|Inc|Ltd|PLC|N\.?V\.?|Holding[s]? NV|Group|"
    r"Technology|Systems|Solutions|Semiconductor|Platforms|ETF)[A-Za-z\.\- ]{0,20}"
    r"\$[\d,\.]+ *[+\-][\d\.]+%")
TAIL = re.compile(r"(더 보기|원문 언어 영어|원본 보기|구독자|· *\d+ *(분|시간|초|일))")

def clean(t):
    t = WIDGET.sub(" ", t)
    t = TAIL.sub(" ", t)
    t = re.sub(r"\s+", " ", t)
    return t

ALIAS = {
    "MU": ["micron"], "SNDK": ["sandisk"], "NBIS": ["nebius"],
    "AAOI": ["applied optoelectronics"], "META": ["meta platforms"],
    "NVDA": ["nvidia"], "LITE": ["lumentum"], "TER": ["teradyne"],
    "ALAB": ["astera labs"], "COHR": ["coherent"], "MRVL": ["marvell"],
    "CRDO": ["credo tech"], "GLW": ["corning"], "AVGO": ["broadcom"],
    "CIEN": ["ciena"], "LRCX": ["lam research"], "AMAT": ["applied materials"],
    "KLAC": ["kla corp", "kla-tencor"], "SKHY": ["sk hynix", "하이닉스"],
    "AMD": ["advanced micro"], "INTC": ["intel"],
    "TSM": ["tsmc", "taiwan semiconductor"], "MSFT": ["microsoft"],
    "GOOGL": ["google", "alphabet", "구글", "알파벳"], "AMZN": ["amazon"],
    "ORCL": ["oracle"], "NOW": ["servicenow"], "CRM": ["salesforce"],
    "ACN": ["accenture"], "JPM": ["jpmorgan", "jp morgan"],
    "CRWV": ["coreweave"], "IREN": ["iris energy"], "AXTI": ["axt inc"],
}

def tickers(t):
    hit = set()
    for tk in UNIV:
        if re.search(r"(?<![A-Za-z])\$?" + tk + r"(?![A-Za-z])", t): hit.add(tk)
    for k in UNIV_KO:
        if k in t: hit.add(k)
    tl = t.lower()
    for tk, names in ALIAS.items():
        for n in names:
            if n in tl: hit.add(tk); break
    return hit

def parse_dt(p):
    if not p: return None
    try: return datetime.datetime.fromisoformat(p.replace("Z", "+00:00"))
    except Exception:
        try:
            from email.utils import parsedate_to_datetime
            return parsedate_to_datetime(p)
        except Exception: return None

def score(item, now):
    raw = item.get("text", "") or ""
    t = clean(raw)
    src = (item.get("source") or item.get("author") or "").strip()
    srcl = src.lower()
    pub = parse_dt(item.get("pub"))
    tags, s = [], 0

    if CUT_PAT.search(t): return None, ["하드컷:노이즈패턴"]
    if blocked(src, item.get("url", "")) or src in BLOCK_X: return None, ["하드컷:소스"]
    tk = tickers(t)
    if KR_CUT.search(t) and not KRREG.search(t) and not tk & {"MU","SNDK","NVDA","META","AAOI","SOXL"}:
        return None, ["하드컷:코스피수급/한국정책"]
    CAPEX_KEEP = re.compile(r"(capex|자본지출|투자액|billion|억달러|조원|\d+ *GW|기가와트|"
                            r"buildout|증설|확대|expansion)", re.I)
    if SUPPLY_CUT.search(t) and not CAPEX_KEEP.search(t):
        return None, ["하드컷:공급제약(전력/부지)"]

    prim = srcl in PRIMARY_SRC or src in PRIMARY_X
    strong = bool(NUMCHG.search(t) and NUMANY.search(t))

    dem = bool(DEMAND_AXIS.search(t) and NUMANY.search(t))
    if not (prim or (strong and tk) or THESIS_BREAK.search(t)
            or END_DEMAND.search(t) or dem or PATRON_FIN.search(t)
            or SUPPLIER_MARGIN.search(t) or OPTICS_TRANSITION.search(t) or CAPEX_UP.search(t)):
        return None, ["게이트 미달"]

    if srcl in DEMOTE_SRC: s -= 2
    if prim: s += 3; tags.append("1차")
    if strong: s += 3; tags.append("숫자")
    elif NUMANY.search(t): s += 1
    if tk: s += 2; tags.append("/".join(sorted(tk)[:4]))
    if pub and (now - pub).total_seconds() < 86400: s += 1
    if pub and (now - pub).total_seconds() > 172800: s -= 2
    if OPINION_ONLY.search(t) and not strong: s -= 3; tags.append("의견만")

    if TECH_GATE.search(t):
        if not (prim or strong): return None, ["기술축 게이트 미달"]
        s += 2; tags.append("기술")
    if STACK_BOTH.search(t) and re.search(r"[\d,\.]+ *(billion|억달러|조|GW|기가와트)", t, re.I):
        s += 2; tags.append("스택양면(반도체+ / 중간상-)")
    if dem: s += 3; tags.append("수요축")
    if END_DEMAND.search(t):
        s += 4; tags.append("!최종수요파괴")
        s = max(s, CUT_X + 1)
    if PATRON_FIN.search(t):
        s += 4; tags.append("!전주지갑")
        s = max(s, CUT_X + 1)
    if TIER1_CXMT.search(t):
        s += 4; tags.append("!티어1中메모리채택")
        s = max(s, CUT_X + 1)
    if THESIS_BREAK.search(t):
        s += 4; tags.append("!논지파괴")
        s = max(s, CUT_X + 1)  # 논지파괴는 소스·채널 불문 무조건 전송
    if AI_MODEL_NAME.search(t):
        if AI_MODEL_EFF.search(t):
            s += 4; tags.append("!AI효율-충격주의")   # 메모리 논지파괴
            s = max(s, CUT_X + 1)
        elif AI_MODEL_AMP.search(t):
            s += 2; tags.append("AI확산-안도")          # 메모리 강화
        if AI_MODEL_SOON.search(t):
            s += 3; tags.append("!AI발표임박")
            s = max(s, CUT_X + 1)
    if SUPPLY_EASE.search(t):
        s += 3; tags.append("!메모리공급완화론")  # 베어 명분, 양면검토 신호
        s = max(s, CUT_X + 1)
    if CHANOS_ACC.search(t):
        s += 4; tags.append("!차노스회계")
        s = max(s, CUT_X + 1)  # 감가·ROIC·미들맨 신호는 소스·채널 불문 전송
    if CAPEX_UP.search(t):
        s += 2; tags.append("capex유지·상향(부족강화)")
    if SUPPLIER_MARGIN.search(t):
        s += 3; tags.append("!공급자마진억제")
        s = max(s, CUT_X + 1)   # 메모리 마진 축 — 강제전송
    if OPTICS_TRANSITION.search(t):
        s += 3; tags.append("!광학전환신호")
        s = max(s, CUT_X + 1)   # 주도플레이어 광학채택 = 광학 촉매
    if EARN.search(t) and tk: s += 3; tags.append("어닝")

    return s, tags

def load(path, hours, now):
    out = []
    if not os.path.exists(path): return out
    for line in open(path, "rb"):
        line = line.decode("utf-8", "replace").strip()
        if not line: continue
        try: d = json.loads(line)
        except Exception: continue
        p = parse_dt(d.get("pub")) or parse_dt(d.get("ts"))
        if p and (now - p).total_seconds() <= hours * 3600:
            out.append(d)
    return out

def main():
    now = datetime.datetime.now(datetime.timezone.utc)
    try: sent = set(json.load(open(STATE)))
    except Exception: sent = set()

    items = [("m", d) for d in load(MEDIA, HOURS, now)] \
            + [("x", d) for d in load(XFEED, HOURS, now)]
    queue, rejected = [], []

    for origin, it in items:
        cut = CUT_X if origin == "x" else CUT
        url = it.get("url", "")
        if not url or url in sent: continue
        t = it.get("text", "") or ""
        src = it.get("source") or it.get("author") or "?"

        ct = clean(t)
        srcl = str(src).lower()
        prim = srcl in PRIMARY_SRC or src in PRIMARY_X
        ch = None
        if KRREG.search(ct): ch = "★계정리스크(한국규제)"
        elif prim and IRAN.search(ct): ch = "이란"
        elif prim and TRUMP.search(ct) and TRUMP_NQ.search(ct): ch = "트럼프/NQ"

        s, tags = score(it, now)
        if ch and (s is None or s < cut):
            s, tags = cut, [ch]
        elif ch:
            tags = [ch] + tags

        if s is None or s < cut:
            rejected.append({"url": url, "src": src, "text": t[:200],
                             "score": 0 if s is None else s,
                             "reason": tags, "ts": now.isoformat()})
            continue
        queue.append((s, tags, src, t, url))

    queue.sort(key=lambda x: -x[0])

    if rejected and not DRY:
        with open(REJECT, "a", encoding="utf-8") as f:
            for r in rejected: f.write(json.dumps(r, ensure_ascii=False) + "\n")

    for s, tags, src, t, url in queue:
        grade = "즉시확인" if s >= 8 else "맥락"
        body = " ".join(t.split())[:400]
        msg = f"[{grade}] {' · '.join(tags)}\n{src}\n{body}\n{url}"
        if DRY: print(msg + "\n" + "·" * 40)
        else:
            if send(msg): sent.add(url); time.sleep(1)

    if not DRY:
        json.dump(list(sent)[-5000:], open(STATE, "w"))
    print(f"[수집 {len(items)} / 전송 {len(queue)} / 컷 {len(rejected)}]")

if __name__ == "__main__":
    main()
