import csv
import hashlib
import html
import json
import re
from datetime import datetime, timezone
from pathlib import Path

from research_inputs import DATA

ROOT = Path(__file__).resolve().parent
OLD = ROOT.parent / 'jp_semicon_pe_20260918'
ANNUAL = ROOT.parent / 'jp_semicon_pe_20260905'
Q1 = ROOT.parent / 'jp_hidden_semicon_20260905'
valuation = json.loads((OLD / 'valuation_inputs.json').read_text())
prior = {c['ticker']: c for c in json.loads((OLD / 'valuation_model.json').read_text())['companies']}
inputs = {c['ticker']: c for c in valuation['companies']}
years = [2024,2025,2026]
labels = ['Worst / 약세','Most likely / 기준','Best / 강세']
colors = ['#176e64','#b77330','#5e76a6','#a04e68','#7e8847','#717b80']

def dump(name, obj):
    (ROOT / name).write_text(json.dumps(obj, ensure_ascii=False, indent=2))

def num(x, decimals=0):
    return '미공개' if x is None else f'{x:,.{decimals}f}'

def pct(x):
    return f'{x*100:+.1f}%'

def table(headers, rows):
    return '\n| ' + ' | '.join(headers) + ' |\n| ' + ' | '.join(['---']*len(headers)) + ' |\n' + ''.join('| ' + ' | '.join(map(str,r)) + ' |\n' for r in rows) + '\n'

def csvfile(name, rows):
    with (ROOT / name).open('w', encoding='utf-8-sig', newline='') as f:
        w=csv.DictWriter(f, fieldnames=list(rows[0])); w.writeheader(); w.writerows(rows)

def composition(c):
    # Three equal-width 100% bars use disclosed segment sums, including truncation.
    out=['<figure><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 880 250" role="img" aria-label="3개년 사업부 매출 구성"><title>3개년 사업부 매출 구성. 회사 내 비중 비교</title><rect width="880" height="250" fill="white"/>']
    for i, year in enumerate(years):
        y=22+i*47; x=108; total=sum(u['sales'][i] for u in c['units'])
        out.append(f'<text x="5" y="{y+24}" font-size="14">FY{year}/3</text>')
        for j,u in enumerate(c['units']):
            width=735*u['sales'][i]/total
            out.append(f'<rect x="{x:.2f}" y="{y}" width="{width:.2f}" height="32" fill="{colors[j]}"/>')
            if width>64:
                out.append(f'<text x="{x+width/2:.2f}" y="{y+22}" text-anchor="middle" fill="white" font-size="13">{u["sales"][i]/total:.1%}</text>')
            x+=width
    for j,u in enumerate(c['units']):
        x=12+(j%3)*285;y=184+(j//3)*28
        out.append(f'<rect x="{x}" y="{y-11}" width="12" height="12" fill="{colors[j]}"/><text x="{x+20}" y="{y}" font-size="13">{html.escape(u["name"])}</text>')
    out.append('</svg><figcaption>막대는 공시 사업부 매출 합계 기준. 표는 전사 매출 기준이므로 절사 차이가 소폭 존재한다.</figcaption></figure>')
    return ''.join(out)

manifest=[]
for base, name in [(ROOT,'sources.json'),(ANNUAL,'primary_manifest.json'),(ANNUAL,'manifest.json'),(Q1,'manifest.json'),(OLD,'update_manifest.json')]:
    path=base/name
    if not path.exists(): continue
    records=json.loads(path.read_text())
    if not isinstance(records,list): continue
    for r in records:
        if not isinstance(r,dict) or not r.get('file'): continue
        p=base/r['file']
        if not p.is_file(): continue
        import os
        rel=os.path.relpath(p,ROOT)
        if any(x['file']==rel for x in manifest): continue
        manifest.append({**r,'file':rel,'sha256':hashlib.sha256(p.read_bytes()).hexdigest()})
for p in [OLD/'valuation_inputs.json',OLD/'valuation_model.json',OLD/'prices.json']:
    import os
    manifest.append(dict(file=os.path.relpath(p,ROOT),url=None,sha256=hashlib.sha256(p.read_bytes()).hexdigest(),kind='retained_model_or_price_input'))
dump('source_manifest.json',manifest)
urlmap={Path(x['file']).name:x.get('url') for x in manifest}

def source_block(t):
    links=[]
    for filename, path, label in [(f'{t}_fy2025.pdf',f'{t}_fy2025.pdf','FY2025 보고서: FY2024·25 비교'),
                                  (f'{t}_annual.pdf',f'../jp_semicon_pe_20260905/{t}_annual.pdf','FY2026 보고서: FY2025·26 비교'),
                                  (f'{t}_q1.pdf',f'../jp_hidden_semicon_20260905/{t}_q1.pdf','FY2027 Q1·회사 전망')]:
        links.append(f'[{label}]({path})' + (f' ([공식 URL]({urlmap[filename]}))' if urlmap.get(filename) else ''))
    return '자료: ' + ' / '.join(links) + '. 재무·부문·제품·주요 고객 표는 위 공식 공시를 직접 전사했다.\n'

actual_rows=[]; product_rows=[]; customer_rows=[]; forecast_rows=[]; scenario_rows=[]; recon=[]; companies=[]
for t,d in DATA.items():
    c={**inputs[t], **d}; c['shares']=c['issued']-c['treasury_q1']+c['subsequent_rsu']
    assert sum(c['weights'])==100
    for i,year in enumerate(years):
        positive=sum(max(0,u['profit'][i]) for u in c['units'])
        sr=c['sales'][i]-sum(u['sales'][i] for u in c['units'])
        pr=c['op'][i]-sum(u['profit'][i] for u in c['units'])-c['adjustment'][i]
        assert abs(sr)<=5,(t,year,'sales',sr)
        assert abs(pr)<=5,(t,year,'profit',pr)
        recon.append(dict(ticker=t,year=year,sales_rounding_residual=sr,profit_adjustment=c['adjustment'][i],profit_rounding_residual=pr,op=c['op'][i]))
        for u in c['units']:
            actual_rows.append(dict(ticker=t,year_ending_march=year,basis='standalone' if t=='4531' else 'consolidated',segment=u['name'],sales=u['sales'][i],sales_share=u['sales'][i]/c['sales'][i],profit=u['profit'][i],margin=u['profit'][i]/u['sales'][i],positive_profit_pool_share=max(0,u['profit'][i])/positive,source=f'{t}_fy2025.pdf' if i==0 else f'../jp_semicon_pe_20260905/{t}_annual.pdf'))
        for p in c['products']:
            product_rows.append(dict(ticker=t,year_ending_march=year,product=p['name'],sales=p['sales'][i],sales_share=p['sales'][i]/c['sales'][i],product_profit=None,source=f'{t}_fy2025.pdf' if i==0 else f'../jp_semicon_pe_20260905/{t}_annual.pdf'))
        if c['products']: assert abs(c['sales'][i]-sum(p['sales'][i] for p in c['products']))<=5
        for name, sales, note in c['customers']:
            customer_rows.append(dict(ticker=t,year_ending_march=year,customer=name,sales=sales[i],share=None if sales[i] is None else sales[i]/c['sales'][i],note=note))
    c['scenarios']=[]
    for k,label in enumerate(labels):
        sales=sum(u['forecast_sales'][k] for u in c['units'])
        op=sum(u['forecast_profit'][k] for u in c['units'])+c['forecast_adjustment'][k]
        assert sales==c['scenario_sales'][k] and op==c['scenario_op'][k],(t,k,sales,op)
        if c['products']: assert sum(p['forecast_sales'][k] for p in c['products'])==sales
        ni=(op+c['scenario_nonop'][k])*.7+c['scenario_nci_loss'][k]
        eps=ni*1e6/c['shares']; target=eps*c['scenario_pe'][k]
        assert abs(target-prior[t]['scenarios'][k]['target'])<1e-8
        s=dict(ticker=t,case=label,weight_pct=c['weights'][k],sales=sales,op=op,op_margin=op/sales,ni=ni,eps=eps,pe=c['scenario_pe'][k],target=target,price=c['price'],upside=target/c['price']-1)
        c['scenarios'].append(s);scenario_rows.append(s)
        for u in c['units']:
            forecast_rows.append(dict(ticker=t,year_ending_march=2028,case=label,level='segment',name=u['name'],sales=u['forecast_sales'][k],sales_share=u['forecast_sales'][k]/sales,profit=u['forecast_profit'][k],margin=u['forecast_profit'][k]/u['forecast_sales'][k],status='analyst_top_down_decomposition'))
        for p in c['products']:
            forecast_rows.append(dict(ticker=t,year_ending_march=2028,case=label,level='product_nested_do_not_add',name=p['name'],sales=p['forecast_sales'][k],sales_share=p['forecast_sales'][k]/sales,profit=None,margin=None,status='analyst_top_down_decomposition'))
    c['weighted_target']=sum(s['target']*s['weight_pct']/100 for s in c['scenarios'])
    c['weighted_upside']=c['weighted_target']/c['price']-1
    c['weight_shift_down']=c['weighted_target']+.1*(c['scenarios'][0]['target']-c['scenarios'][1]['target'])
    c['weight_shift_up']=c['weighted_target']+.1*(c['scenarios'][2]['target']-c['scenarios'][1]['target'])
    c['reverse_op']=(c['price']/c['scenario_pe'][1]*c['shares']/1e6-c['scenario_nci_loss'][1])/.7-c['scenario_nonop'][1]
    companies.append(c)

dump('research_model.json',dict(meta=dict(generated_at=datetime.now(timezone.utc).isoformat(),price_cutoff='2026-09-18 JST 10:59-11:30; frozen intraday snapshot',financial_cutoff='2026-06-30',history='years ending March 2024, 2025, 2026',forecast='normalized year ending March 2028',weight_basis='subjective, uncalibrated analyst scenario weights; not empirical probabilities',forecast_basis='decomposition of retained September 18 top-down scenario anchors, not order-book-derived independent forecasts',unit='JPY million except shares/per-share JPY'),companies=companies,reconciliation=recon))
csvfile('historical_segments.csv',actual_rows);csvfile('historical_products.csv',product_rows)
csvfile('historical_customers.csv',customer_rows);csvfile('forecast_segments_products.csv',forecast_rows)
csvfile('valuation_scenarios.csv',scenario_rows);csvfile('reconciliation.csv',recon)

parts=['# 일본 반도체 상류 소재 6개사\n\n사업부·제품·고객에서 이익으로 | 셀사이드 형식 심층 보고서 | 2026.09.18\n\n금액: 백만 엔(1,000 = 10억 엔). 주가·EPS: 엔. FY는 3월 종료 연도. 유기합성약품만 비연결. 가격은 기존 9/18 오전 장중 스냅샷을 유지하며 이번 작성 시점에 재조회하지 않았다.\n',
'## 1. 투자 결론\n\n**가격 매력은 마나크(4360), 실제 고객과의 연결 근거는 다이토(4366), 날짜가 있는 설비 촉매는 아라카와(4968)가 상대적으로 앞선다. 세 가지를 한 회사의 장점으로 합쳐서는 안 된다.**\n\n이번에는 3개년 공시의 사업부·제품·고객 데이터를 추가했다. 기존 전사 가치평가 숫자는 유지하고 그 숫자가 성립하려면 어느 사업에서 매출·마진이 필요할지 분해했다. 증거의 해상도는 높아졌지만 생산능력·고객 주문 원장을 확보한 것은 아니다. 고객 호황이 공급사 이익으로 바로 연결되지 않는 반례가 중요하다.\n']
parts.append(table(['회사','가격 / JST','기준 가치 / 상승여력','주관적 확률 약/기/강','가중 가치 / 상승여력'],[
    [f'{c["ticker"]} {c["name"]}',f'{num(c["price"])} / {c["quote_time_jst"]}',f'{num(c["scenarios"][1]["target"])} / {pct(c["scenarios"][1]["upside"])}',' / '.join(f'{v}%' for v in c['weights']),f'{num(c["weighted_target"])} / {pct(c["weighted_upside"])}'] for c in companies]))
parts.append('가중 가치는 강세 꼬리의 영향을 받는 산술 평균이며 목표가보다 확실한 숫자가 아니다. 기준 가치에 여유가 없는 다이토·아라카와를 가중 평균 하나로 매수 판단하지 않는다. 배당·비용·원/엔 환율·자금의 시간가치는 상승여력에 포함하지 않았다. 마나크 11:19 거래량 400주의 저유동성도 고려해야 한다. [가격 원자료](../jp_semicon_pe_20260918/prices.json).\n\n- **마나크:** 파인 사업 이익 확대가 전사 턴어라운드를 이끈다. 반도체 고객·반복 주문 공개는 부족하다.\n- **다이토:** 후지필름·도쿄오카 매출과 고객 실적의 연결은 확인된다. 이미 오른 가격과 화성품 저마진을 함께 본다.\n- **아라카와:** FY2024→26 OP 개선의 약 52%에 해당하는 금액이 점접착 적자 축소다. 반도체 신설 공장만의 투자 논리는 불완전하다.\n- **유기합성약품·다오카:** 좋은 고객 실적과 약한 자사 실적이 공존한다. 제품·재고·가동률을 확인하기 전 회복을 선반영하기 어렵다.\n')
parts.append('## 2. 읽는 기준과 확률\n\n실적은 FY2024/3·FY2025/3·FY2026/3의 완료된 연도다. FY2027/3은 2026년 4월~2027년 3월 회사 전망, Q1은 2026년 4~6월이다. 분석자 전망은 FY2028/3 정상화 손익으로 평가한 12~18개월 조건부 가치다. 시점 간 경로를 정밀 예측한 것은 아니다.\n\n사업부 이익은 각 회사 공시 정의이며 본사비·조정 전일 수 있다. 매출 비중은 전사 외부 매출 분모, 마진은 부문 이익/부문 매출이다. **이익 비중은 흑자 부문의 양(+)의 이익 합계 분모**를 사용한다. 적자 부문은 손실 금액을 별도 표시한다. 전사 OP 분모를 쓰면 적자·본사비 때문에 비중이 100%를 넘거나 왜곡될 수 있어 이 정의를 선택했다. 제품군별 이익 미공개는 공란을 매출 비례로 채우지 않았다.\n\n요청에 따라 Worst/Most likely/Best에 주관적 확률을 배정했다. 5%p 단위의 분석자 판단값이며 백테스트·베이지안 추정·통계적 신뢰구간이 아니다. 기준이 각 회사의 최빈 시나리오지만 반드시 50%를 넘는 것은 아니다. 3개 대표 상태에 100%를 배분했을 뿐 실제 결과가 범위 안에 들어올 확률이 100%라는 뜻은 아니다. Worst는 분석 범위의 약세이며 파산·큰 적자·유동성 충격 같은 절대 최악을 포괄하지 않는다.\n\n기존 보고서는 확률을 배정하지 않았으나, 이번에는 사용자 요청에 맞춰 근거와 변경 조건을 공개하는 형태로 추가했다. 전사 매출·OP·PER은 유지했고, 사업부 전망은 그 전사 값을 맞추는 가정 분해다. 따라서 아래 합계 일치는 예측 정확성 검증이 아니라 계산 일관성 검증이다. 모든 미공개 수주·인증·고객 점유율은 여전히 미확인이다.\n')
parts.append('## 3. 고객 호황의 전달 경로\n')
fuji=urlmap['customer_fujifilm_q1.pdf'];tok=urlmap['customer_tok_h1.pdf'];mgc=urlmap['customer_mgc_q1.pdf'];chugai=urlmap['customer_chugai_h1.html']
parts.append(table(['수요 증거','공급사와 관계','확인된 최근 실적','투자 해석과 제한'],[
    [f'[후지필름 공식 Q1]({fuji})','다이토 실제 고객, FY2026 매출 4,220', '2026/4~6 반도체재료 849억 엔, +31.3%; 회사 조정기준 +19.5%', 'AI 관련 반도체 소재 수요는 긍정적. 다이토의 공급 품목·구매량 증가율은 별도'],
    [f'[도쿄오카 공식 H1]({tok})','다이토 실제 고객 1,722; 동방화학은 주주 관계만 확인','2026/1~6 전자기능재료 +27.5%, 전공정 레지스트 +30%, EUV·ArF +35%','최종 공정 성장은 상류 원료의 가능성. 같은 증가율을 공급사 전체에 곱하지 않음'],
    [f'[MGC 공식 Q1]({mgc})','다오카 실제 고객 11,802','2026/4~6 스마트폰 광학 폴리머 견조, BT 수요도 강함','광학·반도체·원료 판가 효과가 혼합. 다오카의 BT 직접 공급은 미확인'],
    [f'[중외제약 공식 H1]({chugai})','유기합성약품 실제 고객 1,622','2026/1~6 매출 +14.7%, Core OP +21.0%','로열티·계약 일시금 기여. 유기합성약품의 의약 Q1 매출 -45.4%와 모순 아님'],
    ['[Seagate 공식 실적 / SEC](https://www.sec.gov/Archives/edgar/data/1137789/000113778926000153/stxq42026pressreleasefinan.htm)','아라카와 확인 고객 아님; HDD 산업 참고','2026/7/3 종료 연도 매출 +34%, 클라우드 데이터센터 수요 견조','아라카와 HDD 연마제 수요 설명과 방향 일치. 공급 계약·점유율은 확인되지 않음']]))
parts.append('**주문 전달의 수치 예시:** 다이토의 FY2026 후지필름+도쿄오카 매출 합계는 5,942다. 이 두 고객향 매출만 10% 늘고 나머지가 같다면 전사 매출 증가는 594.2, 약 3.1%다. 추가 매출의 기여이익률을 임의의 10~20%로 가정하면 OP 증가는 59~119에 그친다. 기준 OP 1,400을 만들려면 FY2026 대비 +528이 필요하므로 더 넓은 물량·믹스·가동률 개선이 요구된다. 이는 민감도 예시이며 공식 주문 전망이나 추가로 합산할 이익이 아니다.\n')

for index,c in enumerate(companies,4):
    t=c['ticker'];parts.append(f'## {index}. {t} {c["name"]}\n\n**{c["thesis"]}**\n\n'+source_block(t))
    parts.append('### 사업과 제품\n'+table(['공시 사업부','제품·최종 산업 / 확인 한계'],[[u['name'],u['products']] for u in c['units']]))
    parts.append('### 전사 3개년\n'+table(['지표','FY2024/3','FY2025/3','FY2026/3'],[
        ['매출',*[num(x) for x in c['sales']]],['매출총이익',*[num(x) for x in c['gp']]],
        ['매출총이익률',*[f'{c["gp"][i]/c["sales"][i]:.1%}' for i in range(3)]],
        ['영업이익',*[num(x) for x in c['op']]],['영업이익률',*[f'{c["op"][i]/c["sales"][i]:.1%}' for i in range(3)]],
        ['순이익'+(' (비연결)' if t=='4531' else ' (지배주주)'),*[num(x) for x in c['ni']]]]))
    parts.append(composition(c)+'\n')
    parts.append('### 사업부 매출과 비중\n'+table(['사업부','FY2024 매출 / 비중','FY2025 매출 / 비중','FY2026 매출 / 비중'],[
        [u['name'],*[f'{num(u["sales"][i])} / {u["sales"][i]/c["sales"][i]:.1%}' for i in range(3)]] for u in c['units']]))
    profitrows=[]
    for u in c['units']:
        row=[u['name']]
        for i in range(3):
            p=u['profit'][i];pool=sum(max(0,x['profit'][i]) for x in c['units'])
            share=f'{p/pool:.1%}' if p>=0 else '적자 제외'
            row.append(f'{num(p)} / {p/u["sales"][i]:.1%} / {share}')
        profitrows.append(row)
    parts.append('### 사업부 이익 구조\n\n각 칸: 부문 이익 / 부문 마진 / 흑자 이익 풀 비중. 손실과 본사 조정은 아래에서 전사 OP로 연결한다.\n'+table(['사업부','FY2024','FY2025','FY2026'],profitrows))
    rr=[r for r in recon if r['ticker']==t]
    parts.append(table(['전사 OP 연결','FY2024','FY2025','FY2026'],[
        ['개별 부문 이익 합',*[num(sum(u['profit'][i] for u in c['units'])) for i in range(3)]],
        ['공시 본사·소거·기타 조정',*[num(x) for x in c['adjustment']]],
        ['공시 절사·표 간 잔차',*[num(r['profit_rounding_residual']) for r in rr]],
        ['전사 영업이익',*[num(x) for x in c['op']]]]))
    if t=='4968':
        parts.append('아라카와 부문 이익에는 영업외손익이 포함될 수 있어 OP와 직접 합산하지 않는다. FY2024/25/26 조정은 각각 공통비 배분차액 +583/+83/+73, 본사 R&D -408/-384/-496, 부문에 포함된 영업외손익 제거 -250/-372/-197의 합이다. 소수 금액 절사 차이는 별도 잔차로 남겼다.\n')
    if c['products']:
        parts.append('### 제품군 매출과 비중\n\n위 사업부의 하위 분해이며 사업부 매출에 다시 더하지 않는다. 세부 제품의 이익·마진은 미공개다. 분석 수탁처럼 공시 사업부와 일치하는 항목의 이익은 위 부문 표 참조.\n'+table(['제품군','FY2024 매출 / 비중','FY2025 매출 / 비중','FY2026 매출 / 비중'],[
            [p['name'],*[f'{num(p["sales"][i])} / {p["sales"][i]/c["sales"][i]:.1%}' for i in range(3)]] for p in c['products']]))
    else: parts.append('추가 세부 제품별 매출·이익의 정량 분해는 확보하지 못했다. 위 부문 매출 전체를 반도체 매출로 간주하지 않는다.\n')
    parts.append(c['evidence']+'\n\n### 특정 고객과 수요\n')
    if c['customers']:
        parts.append(table(['고객 / 거래처','FY2024 매출 / 비중','FY2025 매출 / 비중','FY2026 매출 / 비중','확인 범위'],[
            [name,*['미기재' if s[i] is None else f'{num(s[i])} / {s[i]/c["sales"][i]:.1%}' for i in range(3)],note] for name,s,note in c['customers']]))
        parts.append('미기재는 0이나 신규 고객을 의미하지 않는다. 공시 기준·표시 범위 차이와 임계치 미달 가능성이 있다. 명시된 고객은 법적 거래처이며 최종 팹·최종 사용자와 같지 않을 수 있다.\n')
    parts.append(c['customer_note']+'\n\n### 현재 실적과 회사 전망\n')
    parts.append(table(['구분','매출','영업이익','순이익 / 설명'],[
        ['FY2027 Q1 실제',num(c['q1_sales']),num(c['q1_op']),f'전년 Q1 OP {num(c["prior_q1_op"])}'],
        ['FY2027 회사 연간 전망',num(c['guidance_sales']),num(c['guidance_op']),num(c['guidance_ni'])]]))
    parts.append(c['q1_detail']+'\n\n'+c['current_note']+'\n')
    if t=='4968':
        parts.append('회사 FY2027 부문 전망(분석자 전망과 구분):\n'+table(['부문','매출','부문 이익'],[[n,num(s),num(p)] for n,s,p in zip([u['name'] for u in c['units']],[20500,21400,30000,15000,100],[2400,1100,-400,1100,40])]))
        parts.append('부문 이익 합 4,240 - 본사 신규개발 투자 740 = 회사 자료의 차감후 이익 3,500이다. 이는 공시 전사 OP 3,300이나 경상이익 2,800과 같은 지표가 아니다. OP 대비 순조정 -200이 필요하며, 이름이 다른 이익 지표를 혼용하지 않는다. [공식 Q1 PDF p.18](../jp_hidden_semicon_20260905/4968_q1.pdf#page=18).\n')
    parts.append('### FY2028 사업부 조건부 전망\n\n분석자 가정: 각 칸은 매출 / 이익 / 부문 마진. 기존 전사 시나리오의 사업부 분해이며 회사 발표나 컨센서스가 아니다.\n')
    parts.append(table(['사업부',*labels],[[u['name'],*[f'{num(u["forecast_sales"][k])} / {num(u["forecast_profit"][k])} / {u["forecast_profit"][k]/u["forecast_sales"][k]:.1%}' for k in range(3)]] for u in c['units']]))
    parts.append(table(['합계 연결',*labels],[['본사·조정 가정',*[num(x) for x in c['forecast_adjustment']]],['전사 매출',*[num(x) for x in c['scenario_sales']]],['전사 OP',*[num(x) for x in c['scenario_op']]]]))
    if c['products']:
        parts.append('제품군 매출 전망(이익 추정 없음, 상위 사업부와 중복 합산 금지):\n'+table(['제품군',*labels],[[p['name'],*[f'{num(p["forecast_sales"][k])} / {p["forecast_sales"][k]/c["scenario_sales"][k]:.1%}' for k in range(3)]] for p in c['products']]))
    parts.append(table(['시나리오','주관적 확률','매출·이익 성립 조건'],[[labels[k],f'{c["weights"][k]}%',c['drivers'][k]] for k in range(3)]))
    parts.append('확률 판단: '+c['probability']+'\n\n가중치 변경 조건: '+c['trigger']+'\n\n### 가치평가와 촉매\n')
    parts.append(table(['경우','순익','EPS','PER','조건부 가치','가격 대비'],[[s['case'],num(s['ni']),num(s['eps'],1),f'{s["pe"]}배',num(s['target']),pct(s['upside'])] for s in c['scenarios']]))
    parts.append(f'현재 {num(c["price"])}엔을 기준 PER {c["scenario_pe"][1]}배로 설명하는 역산 OP는 {num(c["reverse_op"])}다. 주관적 가중 가치 {num(c["weighted_target"])}엔은 {pct(c["weighted_upside"])}. 기준 확률 10%p를 약세로 옮기면 {num(c["weight_shift_down"])}엔, 강세로 옮기면 {num(c["weight_shift_up"])}엔이다.\n\n촉매: {c["catalyst"]}\n\n주주·거래 위험: {c["holder"]}\n')
    if t=='4968':parts.append('가치 계산의 비지배 손실 배분은 약세/기준 +500, 강세 +200이다. 기준 +500을 없애면 가치는 약 1,729엔으로 하락한다. JV의 현금 부담을 이 EPS 효과와 별도로 실사해야 한다.\n')

parts.append('## 10. 밸류에이션·주주·거시 가정\n\n수익성의 변화가 큰 소형 화학회사이므로 정상화 EPS×PER를 사용했다. 제품별 이익이 비공개여서 정밀 SOTP를 가장한 제품 가치 합산은 하지 않았다. 적용 PER는 기존 분석자 가정이며 동시점 비교기업에서 통계적으로 추정한 적정 배수가 아니다. 마나크 10/14/18, 동방 9/12/15, 다이토 12/15/18, 아라카와 12/14/16, 유기합성약품 10/12/14, 다오카 12/14/16배다. 저유동성·차입·수익 변동성에는 할인, 반복 고부가 주문 확인에는 높은 배수를 가정한다. 강세는 증익과 재평가를 동시에 요구해 특히 낙관적이다.\n\n순익 = (OP + 영업외손익) × 70% + 비지배 손실 배분. EPS = 순익×1,000,000 / 순주식수. 가치 = EPS×PER. 세율 30%는 통일 가정으로 실제 세효과가 아니다. 순현금·투자유가증권을 PER 가치에 다시 더하지 않았다. 다이토 주식수는 2025/10/1의 1:3 분할 반영 후 기준이다.\n')
parts.append(table(['회사','순주식수','영업외손익 가정','6월 순차입','회사예상 PER','일회성 부분조정 PER','6월 PBR'],[
    [c['ticker'],num(c['shares']),num(c['scenario_nonop'][1]),num(c['debt']-c['cash']),num(c['price']*c['shares']/1e6/c['guidance_ni'],1) if c['guidance_ni'] else '미공개',num(c['price']*c['shares']/1e6/(c['guidance_ni']-c['known_gain_pretax']*.7),1) if c['guidance_ni'] else '미공개',num(c['price']*c['shares']/1e6/c['parent_equity'],2)] for c in companies]))
parts.append('부분조정은 동방 주식 매각익 588, 유기합성약품 230, 아라카와 순보조금 이익 159를 가정 세후로 제외한 값이다. 완전한 정상 이익 검증은 아니다. 주주 표는 2026/3 말 공시 기준이며 9월 실시간 주주명부가 아니다. 이후 대량보유 확인과 거래 가능성, FCF·연금·리스·환경채무는 [기존 9/18 보고서](../jp_semicon_pe_20260918/report.html)의 실사 범위를 유지한다. 유기합성약품 FY2026 보조금 유입 1,854를 반복 영업현금으로 보지 않는다.\n\n거시 기준은 유가의 새 급등이 없고 환율이 추가로 큰 폭 움직이지 않는 조건이다. 이는 확인된 가격 전망이 아니라 분석상 기준 환경이다. 원료가 하락은 매입 원가에 유리할 수 있지만 판가 하락·재고 평가·시차가 상쇄할 수 있다. 엔 강세는 수입 원료에는 유리하고 수출·해외 환산에는 불리할 수 있다. 회사별 원가·통화 민감도가 없으므로 10엔 환율 변화나 유가 10% 변화의 OP 효과를 꾸며 넣지 않았다. 자동차·가전 수요 부진은 난연제·계면활성제·수지첨가제에 잔존하고, AI 호황이 이를 모두 상쇄한다고 가정하지 않는다.\n\n이 보고서는 조건부 공개시장 투자 분석이며 인수 실행 보고서가 아니다. 고객 인증, 계약 변경, 보조금 의무, 모회사 동의, 실제 금융조달을 확인하지 않은 TOB·PE 프리미엄은 0으로 반영했다.\n')
parts.append('## 11. 다음 결산에서 바뀔 판단\n'+table(['회사','우선 확인할 데이터','확인 전 보류할 주장'],[[c['ticker'],c['trigger'],c['catalyst']] for c in companies]))
parts.append('## 12. 데이터·출처·검증\n\n[기존 9/18 보고서](../jp_semicon_pe_20260918/report.html)와 [9/5 원본](../jp_semicon_pe_20260905/report.html)은 변경하지 않았다. EDINETDB는 구조화 데이터 공급자이지 공식 EDINET 자체가 아니다. 이번 사업부·제품·고객 수치는 공식 보고서를 우선했다. 공식 자료의 백만 엔 미만 절사로 합계와 세부 항목 간 차이가 있으며, 이번 전사-사업부 대조의 매출 잔차는 최대 2백만 엔, 이익 잔차는 최대 3백만 엔이다. 이를 임의로 소거하지 않고 보존했다. 제품 분류 변경·인수·원가 배분 변화·환율 효과를 완전히 재작성한 불변 범위 시계열은 아니다.\n\n- [전체 입력과 계산 결과 JSON](research_model.json) / [수기 전사·전망 가정](research_inputs.py) / [생성 코드](build_report.py)\n- [3개년 사업부 CSV](historical_segments.csv) / [제품 CSV](historical_products.csv) / [고객 CSV](historical_customers.csv)\n- [사업부·제품 전망 CSV](forecast_segments_products.csv) / [18개 가치 시나리오 CSV](valuation_scenarios.csv) / [OP·절사 잔차 대조 CSV](reconciliation.csv)\n- [공식 원자료 URL·수집시각·해시](source_manifest.json) / [핵심 PDF 페이지 색인](evidence_pages.json) / [계산 검증](calculation_checks.json) / [화면 검증](qa_results.json)\n\n가격 수집 이후의 변동, 최신 공시 전수검색, 고객별 실제 주문·최종 팹, 제품별 원가·이익, 미발표 인증·TOB는 확인하지 않았다. 고객사 호황 자료는 각기 Q1·H1·다른 결산연도이므로 기간을 맞춘 동행 회귀 데이터가 아니다. Seagate SEC 원문은 웹 열람으로 검증했으며 사이트 직접 다운로드의 리다이렉트 오류로 로컬 원문 보관은 완료하지 못했다. 통계적 확률 검증·기업 IR 인터뷰·고객 실사는 수행하지 않았다.\n')
(ROOT/'report.md').write_text('\n'.join(parts))

evidence=[]
for t in DATA:
    for p in [ROOT/f'{t}_fy2025.txt',ANNUAL/f'{t}_annual.txt']:
        content=p.read_text();blocks=re.split(r'--- (?:PDF )?PAGE (\d+) ---',content)
        for i in range(1,len(blocks),2):
            text=blocks[i+1];tags=[]
            for tag,term in [('segment','セグメント利益'),('customer','主要な顧客'),('product','収益を分解')]:
                if term in text:tags.append(tag)
            if tags:
                import os
                evidence.append(dict(ticker=t,file=os.path.relpath(p.with_suffix('.pdf'),ROOT),pdf_page=int(blocks[i]),tags=tags))
dump('evidence_pages.json',evidence)
dump('calculation_checks.json',dict(status='passed',companies=6,historical_company_years=18,segment_rows=len(actual_rows),product_rows=len(product_rows),customer_rows=len(customer_rows),unchanged_valuation_targets=18,scenario_weights_sum_to_100=True,forecast_sales_and_op_reconcile=True,max_sales_rounding_residual=max(abs(r['sales_rounding_residual']) for r in recon),max_profit_rounding_residual=max(abs(r['profit_rounding_residual']) for r in recon),notes='Arithmetic checks do not validate forecast accuracy or probability calibration.'))
print(json.dumps({'report_characters':len((ROOT/'report.md').read_text()),'companies':[(c['ticker'],round(c['weighted_target']),round(c['weighted_upside']*100,1)) for c in companies]},ensure_ascii=False))
