# -*- coding: utf-8 -*-
"""
APDB weight_min_kg / weight_max_kg 일괄 채우기
- dosage_instructions에서 (사이즈라벨, 체중구간) 쌍을 파싱
- APC 레코드의 product_name에 포함된 사이즈 라벨로 매칭

매칭 전략:
  1. 제품명에 사이즈 라벨(소형견, 중형견 등)이 있으면 → 해당 체중구간 적용
  2. 체중 구간이 1개뿐이면 → 전체 APC에 적용
  3. 다중 구간인데 제품명에 라벨 없으면 → SKIP (안전)

예외 처리:
  - 사료/축산 관련(톤당 kg) → SKIP
  - 축산용(max > 60kg) → SKIP
  - 체중 구간 파싱 불가 → SKIP

실행: python scripts/fill_weight_from_dosage.py [--commit] [--verbose]
  기본: dry-run (DB 변경 없음)
  --commit:  실제 DB 업데이트 수행
  --verbose: 상세 로그
"""
import sys
import io
import re
import argparse

sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
sys.path.insert(0, 'c:\\Users\\청춘약국\\source\\pharmacy-pos-qr-system\\backend')

from sqlalchemy import text, create_engine


# ─────────────────────────────────────────────
# 1. 사이즈 라벨 정의
# ─────────────────────────────────────────────

SIZE_LABELS = {
    '초소형견': 'XS', '초소형': 'XS',
    '소형견': 'S', '소형': 'S',
    '중형견': 'M', '중형': 'M',
    '대형견': 'L', '대형': 'L',
    '초대형견': 'XL', '초대형': 'XL',
}

# 제품명에서 사이즈 감지용 (긴 것부터 먼저 매칭)
PRODUCT_NAME_SIZE_PATTERNS = [
    (r'초소형견', 'XS'),
    (r'초소형', 'XS'),
    (r'소형견', 'S'),
    (r'소형', 'S'),
    (r'중형견', 'M'),
    (r'중형', 'M'),
    (r'초대형견', 'XL'),
    (r'초대형', 'XL'),
    (r'대형견', 'L'),
    (r'대형', 'L'),
    # 영문/약어
    (r'\bSS\b', 'XS'),
    (r'\bXS\b', 'XS'),
    (r'[-\s]S\b', 'S'),
    (r'\bS\(', 'S'),
    (r'[-\s]M\b', 'M'),
    (r'\bM\(', 'M'),
    (r'[-\s]L\b', 'L'),
    (r'\bL\(', 'L'),
    (r'\bXL\b', 'XL'),
]


# ─────────────────────────────────────────────
# 2. 체중 구간 파싱
# ─────────────────────────────────────────────

def strip_html(html_text):
    """HTML 태그 제거, 줄 단위 텍스트 반환"""
    if not html_text:
        return ""
    t = html_text.replace('<p class="indent0">', '\n').replace('</p>', '')
    t = re.sub(r'<[^>]+>', '', t)
    return t


def is_livestock_context(text_content):
    """축산/사료 관련인지 판단"""
    # 톤당 kg은 사료 관련
    if '톤당' in text_content and 'kg' in text_content:
        # 체중 구간이 별도로 있는 경우는 반려동물일 수 있음
        if '체중' not in text_content and '형견' not in text_content:
            return True
    return False


def parse_weight_ranges(dosage_instructions):
    """
    dosage_instructions에서 (사이즈라벨, 체중min, 체중max) 리스트를 추출.
    체중 구간만 추출하며, 성분 용량은 무시.

    Returns:
        list of dict: [{'min': 0, 'max': 11, 'size': 'S', 'label': '소형견'}, ...]
    """
    if not dosage_instructions:
        return []

    txt = strip_html(dosage_instructions)

    # 축산/사료 관련 제외
    if is_livestock_context(txt):
        return []

    ranges = []
    seen = set()  # (size, min, max) 중복 방지

    # ── 전처리: 줄 분리된 사이즈+체중 합치기 ──
    # HTML 변환 후 빈 줄이 끼어있을 수 있음:
    # "소형견  1chewable  68㎍  57mg\n\n(체중0-11kg)"
    # → "소형견  1chewable  68㎍  57mg (체중0-11kg)"
    lines = txt.split('\n')
    # 빈 줄 제거한 리스트 (인덱스 보존)
    non_empty = [(i, line.strip()) for i, line in enumerate(lines) if line.strip()]

    merged_set = set()  # 합쳐진 줄 인덱스 (원본 기준)
    merged_lines = []

    for idx, (orig_i, stripped) in enumerate(non_empty):
        if orig_i in merged_set:
            continue
        # 현재 줄에 사이즈 라벨이 있고, 다음 비어있지 않은 줄이 (체중...) 패턴이면 합치기
        if idx + 1 < len(non_empty):
            next_orig_i, next_stripped = non_empty[idx + 1]
            if (re.match(r'\(체중', next_stripped)
                    and re.search(r'(초소형|소형|중형|대형|초대형)견?', stripped)):
                merged_lines.append(stripped + ' ' + next_stripped)
                merged_set.add(next_orig_i)
                continue
        merged_lines.append(stripped)

    txt = '\n'.join(merged_lines)

    def add_range(size, wmin, wmax, label):
        """중복 방지하며 범위 추가"""
        if wmax > 60:  # 반려동물 체중 범위 초과 → 축산용
            return
        if wmax <= wmin:
            return
        key = (size, wmin, wmax)
        if key not in seen:
            seen.add(key)
            ranges.append({'min': wmin, 'max': wmax, 'size': size, 'label': label})

    def get_size(label_text):
        """라벨 텍스트 → 사이즈 코드"""
        return SIZE_LABELS.get(label_text + '견', SIZE_LABELS.get(label_text))

    # ── 패턴1: "X형견(체중A-Bkg)" / "X형견 ... (체중A-Bkg)" ──
    # 예: "소형견(체중0-11kg)", "중형견(체중12-22kg)"
    # 예(줄 합침): "소형견  1chewable 68㎍ 57mg (체중0-11kg)"
    for m in re.finditer(
        r'(초소형|소형|중형|대형|초대형)견?\s*(?:용\s*)?\(?(?:체중\s*)?'
        r'(\d+\.?\d*)\s*[-~～]\s*(\d+\.?\d*)\s*kg',
        txt
    ):
        label = m.group(1)
        add_range(get_size(label), float(m.group(2)), float(m.group(3)), label + '견')

    # ── 패턴1b: 같은 줄에 라벨과 (체중...)이 먼 경우 ──
    # 예: "소형견       1chewable   68㎍      57mg    1개월 (체중0-11kg)"
    for m in re.finditer(
        r'(초소형|소형|중형|대형|초대형)견?\b[^\n]*?\(체중\s*'
        r'(\d+\.?\d*)\s*[-~～]\s*(\d+\.?\d*)\s*kg\)',
        txt
    ):
        label = m.group(1)
        add_range(get_size(label), float(m.group(2)), float(m.group(3)), label + '견')

    # ── 패턴2: "체중A~Bkg X형견용" ──
    # 예: "체중12～22kg 중형견용(M)"
    for m in re.finditer(
        r'(?:체중\s*)?(\d+\.?\d*)\s*[-~～]\s*(\d+\.?\d*)\s*kg\s*(?:의\s*)?'
        r'(초소형|소형|중형|대형|초대형)견?',
        txt
    ):
        label = m.group(3)
        add_range(get_size(label), float(m.group(1)), float(m.group(2)), label + '견')

    # ── 패턴3: "Akg이하 X형견" / "~Akg X형견" ──
    # 예: "11kg이하 소형견용"
    for m in re.finditer(
        r'(?:체중\s*)?[~～]?\s*(\d+\.?\d*)\s*kg\s*(?:이하|까지)?\s*(?:의\s*)?'
        r'(초소형|소형|중형|대형|초대형)견?',
        txt
    ):
        label = m.group(2)
        add_range(get_size(label), 0, float(m.group(1)), label + '견')

    # ── 패턴4: "(Akg~Bkg의 X형견에게)" ──
    # 예: "(5.7kg ~11kg의 소형견에게 본제 1정 투여)"
    for m in re.finditer(
        r'\(\s*(\d+\.?\d*)\s*kg?\s*[-~～]\s*(\d+\.?\d*)\s*kg\s*(?:의\s*)?'
        r'(초소형|소형|중형|대형|초대형)견',
        txt
    ):
        label = m.group(3)
        add_range(get_size(label), float(m.group(1)), float(m.group(2)), label + '견')

    # ── 패턴4b: "(Akg.Bkg의 X형견에게)" - 마침표 구분자 ──
    # 예: "(12kg.22kg의 중형견에게)"
    for m in re.finditer(
        r'\(\s*(\d+\.?\d*)\s*kg\s*\.\s*(\d+\.?\d*)\s*kg\s*(?:의\s*)?'
        r'(초소형|소형|중형|대형|초대형)견',
        txt
    ):
        label = m.group(3)
        add_range(get_size(label), float(m.group(1)), float(m.group(2)), label + '견')

    # ── 패턴5: "Akg이하의 X형견에게" ──
    # 예: "(5.6kg이하의 초소형견에게)"
    for m in re.finditer(
        r'(\d+\.?\d*)\s*kg\s*이하\s*(?:의\s*)?(초소형|소형|중형|대형|초대형)견',
        txt
    ):
        label = m.group(2)
        add_range(get_size(label), 0, float(m.group(1)), label + '견')

    # ── 패턴6: 테이블 "A~B | 제품명 X형견용" ──
    # 예: "2~3.5  넥스가드 스펙트라 츄어블 초소형견용  1"
    # 예: "2-5   | 프론트라인 트리액트 초소형견용 | 0.5ml"
    for m in re.finditer(
        r'(\d+\.?\d*)\s*[-~～]\s*(\d+\.?\d*)\s*[\s|]*[^\n]*?'
        r'(초소형|소형|중형|대형|초대형)견?용?',
        txt
    ):
        label = m.group(3)
        wmin, wmax = float(m.group(1)), float(m.group(2))
        if wmax <= 60:
            add_range(get_size(label), wmin, wmax, label + '견')

    # ── 패턴7: "체중 Akg 미만 X형견용" ──
    # 예: "체중 15kg 미만 소, 중형견용"
    for m in re.finditer(
        r'체중\s*(\d+\.?\d*)\s*kg\s*미만\s*[^\n]*?'
        r'(초소형|소형|중형|대형|초대형)견',
        txt
    ):
        label = m.group(2)
        add_range(get_size(label), 0, float(m.group(1)), label + '견')

    # ── 패턴8: 라벨 없이 체중 구간만 (반려동물 키워드 있을 때) ──
    if not ranges and ('개' in txt or '고양이' in txt or '반려' in txt or '애완' in txt):
        # 8a: "Xkg 초과-Ykg 이하" / "Xkg 초과 ~ Ykg" (먼저 처리)
        for m in re.finditer(
            r'(\d+\.?\d*)\s*kg\s*초과\s*[-~～]?\s*(\d+\.?\d*)\s*kg(?:\s*(?:이하|까지))?',
            txt
        ):
            wmin, wmax = float(m.group(1)), float(m.group(2))
            if wmax <= 60 and wmax > wmin:
                add_range(None, wmin, wmax, None)

        # 8b: "X-Ykg" / "X~Ykg" 일반 범위
        for m in re.finditer(
            r'(?:체중\s*)?(\d+\.?\d*)\s*[-~～]\s*(\d+\.?\d*)\s*kg',
            txt
        ):
            wmin, wmax = float(m.group(1)), float(m.group(2))
            if wmax <= 60 and wmax > wmin:
                add_range(None, wmin, wmax, None)

        # 8c: "Xkg 이하" / "~Xkg" (최소=0)
        # 단, "Akg 초과-Xkg 이하"는 8a에서 이미 처리되었으므로 제외
        for m in re.finditer(
            r'(?:체중\s*)?[~～]\s*(\d+\.?\d*)\s*kg|(\d+\.?\d*)\s*kg\s*(?:이하|까지)',
            txt
        ):
            val = m.group(1) or m.group(2)
            wmax = float(val)
            if wmax <= 60:
                # "초과-Xkg 이하" 컨텍스트인지 확인 → 이미 8a에서 처리됨
                start = max(0, m.start() - 15)
                before = txt[start:m.start()]
                if '초과' in before:
                    continue
                add_range(None, 0, wmax, None)

    # 정렬 (min 기준)
    ranges.sort(key=lambda x: x['min'])
    return ranges


def is_multi_size_product_name(product_name):
    """
    제품명에 여러 사이즈가 함께 들어있는 통합 제품인지 판단.
    예: "하트커버(SS,S,M,L)정" → True
    """
    if not product_name:
        return False
    # 여러 사이즈 약어가 한 제품명에 있는 경우
    if re.search(r'[(\(].*(?:SS|XS).*[,/].*(?:S|M|L).*[)\)]', product_name):
        return True
    # 소형/중형/대형 등이 2개 이상 포함된 경우
    size_count = sum(1 for kw in ['초소형', '소형', '중형', '대형', '초대형']
                     if kw in product_name)
    if size_count >= 2:
        return True
    return False


def detect_size_from_product_name(product_name):
    """
    제품명에서 사이즈 라벨을 감지.
    Returns: 'XS', 'S', 'M', 'L', 'XL' 또는 None
    통합 제품(SS,S,M,L 등 여러 사이즈)은 None 반환.
    """
    if not product_name:
        return None
    # 통합 제품 제외
    if is_multi_size_product_name(product_name):
        return None
    for pattern, size in PRODUCT_NAME_SIZE_PATTERNS:
        if re.search(pattern, product_name):
            return size
    return None


# ─────────────────────────────────────────────
# 3. 메인 로직
# ─────────────────────────────────────────────

def main():
    parser = argparse.ArgumentParser(description='APDB weight_min_kg/weight_max_kg 일괄 채우기')
    parser.add_argument('--commit', action='store_true', help='실제 DB 업데이트 수행 (기본: dry-run)')
    parser.add_argument('--verbose', '-v', action='store_true', help='상세 로그')
    args = parser.parse_args()

    dry_run = not args.commit

    if dry_run:
        print("=" * 60)
        print("  DRY-RUN 모드 (DB 변경 없음)")
        print("  실제 업데이트: python scripts/fill_weight_from_dosage.py --commit")
        print("=" * 60)
    else:
        print("=" * 60)
        print("  COMMIT 모드 - DB에 실제 업데이트합니다")
        print("=" * 60)

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

    # ── 동물용의약품 중 dosage_instructions에 kg 있고 weight 미입력인 APC 전체 조회 ──
    apcs = conn.execute(text('''
        SELECT apc, item_seq, product_name, dosage, dosage_instructions,
               product_type
        FROM apc
        WHERE product_type = '동물용의약품'
          AND dosage_instructions ILIKE '%%kg%%'
          AND weight_min_kg IS NULL
        ORDER BY item_seq, apc
    ''')).fetchall()

    print(f'\n대상 APC 레코드: {len(apcs)}건')

    # item_seq별로 그룹핑
    from collections import defaultdict
    items = defaultdict(list)
    di_cache = {}
    for row in apcs:
        items[row.item_seq].append(row)
        if row.item_seq not in di_cache:
            di_cache[row.item_seq] = row.dosage_instructions

    print(f'대상 item_seq: {len(items)}건\n')

    stats = {
        'total_items': len(items),
        'updated': 0,
        'matched_by_name': 0,
        'matched_by_dosage_order': 0,
        'matched_single': 0,
        'skipped_no_parse': 0,
        'skipped_livestock': 0,
        'skipped_multi_no_label': 0,
    }

    updates = []  # (apc, weight_min, weight_max, product_name, reason)

    for item_seq, apc_rows in items.items():
        di = di_cache[item_seq]
        first_name = apc_rows[0].product_name

        # 체중 구간 파싱
        weight_ranges = parse_weight_ranges(di)

        if not weight_ranges:
            stats['skipped_no_parse'] += 1
            if args.verbose:
                print(f'  SKIP (파싱불가): {first_name} ({item_seq})')
            continue

        # 축산용 필터 (max > 60kg인 구간이 있으면 전체 SKIP)
        if any(r['max'] > 60 for r in weight_ranges):
            stats['skipped_livestock'] += 1
            if args.verbose:
                large = [r for r in weight_ranges if r['max'] > 60]
                print(f'  SKIP (축산용): {first_name} ({item_seq}) max={large[0]["max"]}kg')
            continue

        if len(weight_ranges) == 1:
            # ── 체중 구간 1개 → 전체 APC에 적용 ──
            wr = weight_ranges[0]
            for row in apc_rows:
                updates.append((row.apc, wr['min'], wr['max'], row.product_name, '단일구간'))
            stats['matched_single'] += len(apc_rows)
            stats['updated'] += len(apc_rows)
            if args.verbose:
                print(f'  적용 (단일구간): {first_name} → {wr["min"]}~{wr["max"]}kg ({len(apc_rows)}건)')

        else:
            # ── 체중 구간 여러 개 → 제품명의 사이즈 라벨로 매칭 ──
            size_to_weight = {}
            for wr in weight_ranges:
                if wr['size']:
                    size_to_weight[wr['size']] = (wr['min'], wr['max'])

            # 먼저 제품명 라벨로 매칭 시도
            unmatched_rows = []
            for row in apc_rows:
                size = detect_size_from_product_name(row.product_name)
                if size and size in size_to_weight:
                    wmin, wmax = size_to_weight[size]
                    updates.append((row.apc, wmin, wmax, row.product_name, f'제품명→{size}'))
                    stats['matched_by_name'] += 1
                    stats['updated'] += 1
                    if args.verbose:
                        print(f'  적용 (제품명 {size}): {row.product_name} → {wmin}~{wmax}kg')
                else:
                    unmatched_rows.append(row)

            # ── 제품명 매칭 실패한 것들 → dosage 순서 매칭 시도 ──
            if unmatched_rows:
                # dosage 값이 있는 APC만 추출 (NaN 제외)
                rows_with_dosage = [r for r in unmatched_rows
                                    if r.dosage and r.dosage != 'NaN']
                rows_no_dosage = [r for r in unmatched_rows
                                  if not r.dosage or r.dosage == 'NaN']

                if rows_with_dosage and len(weight_ranges) >= 2:
                    # dosage에서 첫 번째 숫자 추출하여 정렬 키로 사용
                    def dosage_sort_key(dosage_str):
                        nums = re.findall(r'(\d+\.?\d+)', dosage_str)
                        return float(nums[0]) if nums else 0

                    # 고유 dosage 값 추출 (순서 유지)
                    unique_dosages = sorted(
                        set(r.dosage for r in rows_with_dosage),
                        key=dosage_sort_key
                    )
                    # 체중 구간도 min 기준 정렬 (이미 정렬됨)
                    sorted_ranges = sorted(weight_ranges, key=lambda x: x['min'])

                    if len(unique_dosages) == len(sorted_ranges):
                        # 개수 일치 → 순서 매칭 (작은 용량 = 작은 체중)
                        dosage_to_weight = {}
                        for d, wr in zip(unique_dosages, sorted_ranges):
                            dosage_to_weight[d] = (wr['min'], wr['max'])

                        for row in rows_with_dosage:
                            if row.dosage in dosage_to_weight:
                                wmin, wmax = dosage_to_weight[row.dosage]
                                updates.append((row.apc, wmin, wmax, row.product_name,
                                                f'dosage순서→{wmin}~{wmax}'))
                                stats['matched_by_dosage_order'] += 1
                                stats['updated'] += 1
                                if args.verbose:
                                    print(f'  적용 (dosage순서): {row.product_name} '
                                          f'dosage={row.dosage} → {wmin}~{wmax}kg')
                            else:
                                stats['skipped_multi_no_label'] += 1
                                if args.verbose:
                                    print(f'  SKIP (dosage매칭실패): {row.product_name}')

                        # dosage 없는 APC (대표 품목 등)
                        for row in rows_no_dosage:
                            stats['skipped_multi_no_label'] += 1
                            if args.verbose:
                                print(f'  SKIP (다중구간+dosage없음): {row.product_name}')

                        if args.verbose and dosage_to_weight:
                            print(f'    dosage 매핑: {dict((d, f"{w[0]}~{w[1]}kg") for d, w in dosage_to_weight.items())}')
                    else:
                        # 개수 불일치 → SKIP
                        for row in unmatched_rows:
                            stats['skipped_multi_no_label'] += 1
                            if args.verbose:
                                print(f'  SKIP (dosage수≠구간수): {row.product_name} '
                                      f'(dosage {len(unique_dosages)}종 vs 구간 {len(sorted_ranges)}개)')
                else:
                    # dosage 없는 APC만 남음
                    for row in unmatched_rows:
                        stats['skipped_multi_no_label'] += 1
                        if args.verbose:
                            print(f'  SKIP (다중구간+라벨없음): {row.product_name} '
                                  f'(감지={detect_size_from_product_name(row.product_name)}, '
                                  f'가용={list(size_to_weight.keys())})')

    # ── 결과 출력 ──
    print('\n' + '=' * 60)
    print('  결과 요약')
    print('=' * 60)
    print(f'  대상 item_seq:            {stats["total_items"]}건')
    print(f'  업데이트할 APC:           {stats["updated"]}건')
    print(f'    - 단일구간 적용:        {stats["matched_single"]}건')
    print(f'    - 제품명 라벨 매칭:     {stats["matched_by_name"]}건')
    print(f'    - dosage 순서 매칭:     {stats["matched_by_dosage_order"]}건')
    print(f'  SKIP - 파싱 불가:         {stats["skipped_no_parse"]}건')
    print(f'  SKIP - 축산용 (>60kg):    {stats["skipped_livestock"]}건')
    print(f'  SKIP - 다중구간+라벨없음: {stats["skipped_multi_no_label"]}건')

    if updates:
        print(f'\n  === 업데이트 미리보기 (처음 30건) ===')
        for apc, wmin, wmax, pname, reason in updates[:30]:
            print(f'    {apc} | {pname[:35]:35s} → {wmin}~{wmax}kg [{reason}]')
        if len(updates) > 30:
            print(f'    ... 외 {len(updates) - 30}건')

    # ── DB 업데이트 ──
    if not dry_run and updates:
        print(f'\n  DB 업데이트 시작...')
        conn.close()
        with pg.begin() as tx_conn:
            for apc_code, wmin, wmax, _, _ in updates:
                tx_conn.execute(text('''
                    UPDATE apc
                    SET weight_min_kg = :wmin, weight_max_kg = :wmax
                    WHERE apc = :apc
                '''), {'wmin': wmin, 'wmax': wmax, 'apc': apc_code})
        print(f'  완료: {len(updates)}건 업데이트')
    elif not dry_run and not updates:
        print('\n  업데이트할 항목이 없습니다.')
        conn.close()
    else:
        conn.close()
    print('\n완료.')


if __name__ == '__main__':
    main()
