# -*- coding: utf-8 -*-
"""
동물약 일괄 APC 매칭 (개선판)
- 띄어쓰기 무시 매칭
- 체중 범위로 정밀 매칭
- dry-run 모드 (검증용)
"""
import sys, io, re
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
sys.path.insert(0, 'c:\\Users\\청춘약국\\source\\pharmacy-pos-qr-system\\backend')

from db.dbsetup import get_db_session
from sqlalchemy import text, create_engine
from datetime import datetime

DRY_RUN = True  # True: 검증만, False: 실제 INSERT

# ── 유틸 함수 ──

def normalize(name):
    """띄어쓰기/특수문자 제거하여 비교용 문자열 생성"""
    # 공백, 하이픈, 점 제거
    return re.sub(r'[\s\-\.]+', '', name).lower()

def extract_base_name(mssql_name):
    """MSSQL 제품명에서 검색용 기본명 추출 (여러 후보 반환)
    예: '다이로하트정M(12~22kg)' → ['다이로하트정', '다이로하트']
        '하트캅츄어블(11kg이하)' → ['하트캅츄어블', '하트캅']
        '클라펫정50(100정)' → ['클라펫정50', '클라펫정', '클라펫']
    """
    name = mssql_name.replace('(판)', '')
    # 사이즈 라벨(XS/SS/S/M/L/XL/mini) + 괄호 이전까지
    m = re.match(r'^(.+?)(XS|SS|XL|xs|mini|S|M|L)?\s*[\(/]', name)
    if m:
        base = m.group(1)
    else:
        base = re.sub(r'[\(/].*', '', name)
    base = base.strip()

    candidates = [base]
    # 끝의 숫자 제거: 클라펫정50 → 클라펫정
    no_num = re.sub(r'\d+$', '', base)
    if no_num and no_num != base:
        candidates.append(no_num)
    # 제형 접미사 제거: 다이로하트정 → 다이로하트, 하트캅츄어블 → 하트캅
    for suffix in ['츄어블', '정', '액', '캡슐', '산', '시럽']:
        for c in list(candidates):
            stripped = re.sub(suffix + r'$', '', c)
            if stripped and stripped != c and stripped not in candidates:
                candidates.append(stripped)
    return candidates

def extract_weight_range(mssql_name):
    """MSSQL 제품명에서 체중 범위 추출
    '가드닐L(20~40kg)' → (20, 40)
    '셀라이트액SS(2.5kg이하)' → (0, 2.5)
    '파라캅L(5kg이상)' → (5, 999)
    '하트웜솔루션츄어블S(11kg이하)' → (0, 11)
    '다이로하트정S(5.6~11kg)' → (5.6, 11)
    """
    # 범위: (5.6~11kg), (2~10kg)
    m = re.search(r'\((\d+\.?\d*)[-~](\d+\.?\d*)\s*kg\)', mssql_name)
    if m:
        return float(m.group(1)), float(m.group(2))

    # 이하: (2.5kg이하), (11kg이하)
    m = re.search(r'\((\d+\.?\d*)\s*kg\s*이하\)', mssql_name)
    if m:
        return 0, float(m.group(1))

    # 이상: (5kg이상)
    m = re.search(r'\((\d+\.?\d*)\s*kg\s*이상\)', mssql_name)
    if m:
        return float(m.group(1)), 999

    return None, None

def weight_match(mssql_min, mssql_max, pg_min, pg_max):
    """체중 범위가 일치하는지 확인 (약간의 오차 허용)"""
    if pg_min is None or pg_max is None:
        return False
    # 이상(999)인 경우 pg_max도 큰 값이면 OK
    if mssql_max == 999 and pg_max >= 50:
        return abs(mssql_min - pg_min) <= 1
    return abs(mssql_min - pg_min) <= 1 and abs(mssql_max - pg_max) <= 1


# ── 1. MSSQL 동물약 (APC 없는 것만) ──

session = get_db_session('PM_DRUG')
result = session.execute(text("""
    SELECT
        G.DrugCode,
        G.GoodsName,
        G.Saleprice,
        (
            SELECT TOP 1 U.CD_CD_BARCODE
            FROM CD_ITEM_UNIT_MEMBER U
            WHERE U.DRUGCODE = G.DrugCode
              AND U.CD_CD_BARCODE LIKE '023%'
        ) AS APC_CODE
    FROM CD_GOODS G
    WHERE G.POS_BOON = '010103'
      AND G.GoodsSelCode = 'B'
    ORDER BY G.GoodsName
"""))

no_apc = []
for row in result:
    if not row.APC_CODE:
        no_apc.append({
            'code': row.DrugCode,
            'name': row.GoodsName,
            'price': row.Saleprice
        })

session.close()

print(f'=== APC 없는 동물약: {len(no_apc)}개 ===')
print(f'=== 모드: {"DRY-RUN (검증만)" if DRY_RUN else "실제 INSERT"} ===\n')

# ── 2. PostgreSQL에서 매칭 ──

pg = create_engine('postgresql://admin:trajet6640@192.168.0.87:5432/apdb_master').connect()

matched = []      # 확정 매칭
ambiguous = []    # 후보 여러 개 (수동 확인 필요)
no_match = []     # 매칭 없음

for drug in no_apc:
    name = drug['name']
    base_names = extract_base_name(name)
    w_min, w_max = extract_weight_range(name)

    # 여러 기본명 후보로 검색 (좁은 것부터 시도)
    candidates = []
    used_base = None
    for bn in base_names:
        norm_base = normalize(bn)
        result = pg.execute(text("""
            SELECT apc, product_name,
                   weight_min_kg, weight_max_kg,
                   dosage,
                   llm_pharm->>'사용가능 동물' as target
            FROM apc
            WHERE REGEXP_REPLACE(LOWER(product_name), '[\\s\\-\\.]+', '', 'g') LIKE :pattern
            ORDER BY product_name
        """), {'pattern': f'%{norm_base}%'})
        candidates = list(result)
        if candidates:
            used_base = bn
            break
    if not used_base:
        used_base = base_names[0]

    if not candidates:
        no_match.append(drug)
        print(f'❌ {name}')
        print(f'   기본명: {base_names} → 매칭 없음')
        continue

    # ── 단계별 필터링 ──

    # (A) 제형 필터: MSSQL 이름에 "정"이 있으면 PG에서도 "정" 포함 우선
    filtered = candidates
    for form in ['정', '액', '캡슐']:
        if form in name.split('(')[0]:
            form_match = [c for c in filtered if form in c.product_name]
            if form_match:
                filtered = form_match
                break

    # (B) 체중 범위로 정밀 매칭
    if w_min is not None:
        exact = [c for c in filtered
                 if weight_match(w_min, w_max, c.weight_min_kg, c.weight_max_kg)]
        if exact:
            filtered = exact

    # (C) 포장단위 여러 개면 최소 포장 선택 (낱개 판매 기준)
    #     "/ 6 정", "/ 1 피펫" 등에서 숫자 추출
    if len(filtered) > 1:
        def extract_pack_qty(pname):
            m = re.search(r'/\s*(\d+)\s*(정|피펫|개|포)', pname)
            return int(m.group(1)) if m else 0
        has_qty = [(c, extract_pack_qty(c.product_name)) for c in filtered]
        # 포장수량이 있는 것들만 필터
        with_qty = [(c, q) for c, q in has_qty if q > 0]
        if with_qty:
            min_qty = min(q for _, q in with_qty)
            filtered = [c for c, q in with_qty if q == min_qty]

    # (D) 그래도 여러 개면 대표 APC (product_name이 가장 짧은 것) 선택
    if len(filtered) > 1:
        # 포장수량 정보가 없는 대표 코드가 있으면 우선
        no_qty = [c for c in filtered if '/' not in c.product_name]
        if len(no_qty) == 1:
            filtered = no_qty

    # ── 결과 판정 ──
    if len(filtered) == 1:
        method = '체중매칭' if w_min is not None and filtered[0].weight_min_kg is not None else '유일후보'
        matched.append({
            'mssql': drug,
            'apc': filtered[0],
            'method': method
        })
        print(f'✅ {name}')
        print(f'   → {filtered[0].apc}: {filtered[0].product_name}')
        if w_min is not None and filtered[0].weight_min_kg is not None:
            print(f'   체중: MSSQL({w_min}~{w_max}kg) = PG({filtered[0].weight_min_kg}~{filtered[0].weight_max_kg}kg)')
        continue

    # 후보가 0개 (필터가 너무 강했으면 원래 candidates로 복구)
    if len(filtered) == 0:
        filtered = candidates

    # 수동 확인
    ambiguous.append({
        'mssql': drug,
        'candidates': filtered,
        'reason': f'후보 {len(filtered)}건'
    })
    print(f'⚠️ {name} - 후보 {len(filtered)}건 (수동 확인)')
    for c in filtered[:5]:
        wt = f'({c.weight_min_kg}~{c.weight_max_kg}kg)' if c.weight_min_kg else ''
        print(f'   → {c.apc}: {c.product_name} {wt}')

pg.close()

# ── 3. 요약 ──

print(f'\n{"="*50}')
print(f'=== 매칭 요약 ===')
print(f'APC 없는 제품: {len(no_apc)}개')
print(f'✅ 확정 매칭: {len(matched)}개')
print(f'⚠️ 수동 확인: {len(ambiguous)}개')
print(f'❌ 매칭 없음: {len(no_match)}개')

if matched:
    print(f'\n{"="*50}')
    print(f'=== 확정 매칭 목록 (INSERT 대상) ===')
    for m in matched:
        d = m['mssql']
        a = m['apc']
        print(f'  {d["name"]:40s} → {a.apc} [{m["method"]}]')

# ── 4. INSERT (DRY_RUN=False일 때만) ──

if matched and not DRY_RUN:
    print(f'\n{"="*50}')
    print(f'=== INSERT 실행 ===')
    session = get_db_session('PM_DRUG')
    today = datetime.now().strftime('%Y%m%d')

    for m in matched:
        drugcode = m['mssql']['code']
        apc = m['apc'].apc

        # 기존 가격 조회
        existing = session.execute(text("""
            SELECT TOP 1 CD_MY_UNIT, CD_IN_UNIT
            FROM CD_ITEM_UNIT_MEMBER
            WHERE DRUGCODE = :dc
            ORDER BY SN DESC
        """), {'dc': drugcode}).fetchone()

        if not existing:
            print(f'  ❌ {m["mssql"]["name"]}: 기존 레코드 없음')
            continue

        # 중복 확인
        check = session.execute(text("""
            SELECT 1 FROM CD_ITEM_UNIT_MEMBER
            WHERE DRUGCODE = :dc AND CD_CD_BARCODE = :apc
        """), {'dc': drugcode, 'apc': apc}).fetchone()

        if check:
            print(f'  ⏭️ {m["mssql"]["name"]}: 이미 등록됨')
            continue

        try:
            session.execute(text("""
                INSERT INTO CD_ITEM_UNIT_MEMBER (
                    DRUGCODE, CD_CD_UNIT, CD_NM_UNIT, CD_MY_UNIT, CD_IN_UNIT,
                    CD_CD_BARCODE, CD_CD_POS, CHANGE_DATE
                ) VALUES (
                    :drugcode, '015', 1.0, :my_unit, :in_unit,
                    :barcode, '', :change_date
                )
            """), {
                'drugcode': drugcode,
                'my_unit': existing.CD_MY_UNIT,
                'in_unit': existing.CD_IN_UNIT,
                'barcode': apc,
                'change_date': today
            })
            session.commit()
            print(f'  ✅ {m["mssql"]["name"]} → {apc}')
        except Exception as e:
            session.rollback()
            print(f'  ❌ {m["mssql"]["name"]}: {e}')

    session.close()
    print('\n완료!')
