"""journal_validator.py — Jamie Journal Editor validation tool.

Usage:
    python journal_validator.py --input article.md
    python journal_validator.py --input article.md --check structure,compliance
    python journal_validator.py --input article.md --output report.json --verbose

Checks available: structure, compliance, spelling, links (default: all)

Exit codes:
    0 — validation passed (no critical/high issues)
    1 — validation failed (critical or high issues found)
    2 — input error (file not found, bad arguments, etc.)
"""

import re
import json
import sys
from typing import Dict, List, Tuple, Optional
from dataclasses import dataclass, asdict, field
import argparse


# ---------------------------------------------------------------------------
# Data model
# ---------------------------------------------------------------------------

@dataclass
class ValidationIssue:
    checker: str
    issue_type: str
    severity: str          # critical | high | medium | info
    line_number: int
    matched_text: str
    message: str
    suggestion: str


# ---------------------------------------------------------------------------
# Stub checker classes
# ---------------------------------------------------------------------------

class StructureValidator:
    """Validates document structure (headings, sections, word count, etc.)."""

    OPENING = "안녕하세요. 제이미성형외과 정기호 원장입니다."

    STEP_KEYWORDS = {
        'problem':    ['고민', '증상', '불편', '걱정', '힘드', '어려움'],
        'cause':      ['원인', '발생', '때문에', '이유', '구조적', '해부학적'],
        'solution':   ['시술', '수술', '방법', '제이미', '교정', '개선법'],
        'advantages': ['장점', '회복', '흉터', '통증', '마취', '보장', 'AS'],
        'results':    ['결과', '효과', '기대', '개선', '자연스러', '만족'],
    }

    STEP_NAMES = {
        'problem':    '문제 제기 (공감)',
        'cause':      '원인 설명 (교육)',
        'solution':   '해결책 제시 (제이미의 방법)',
        'advantages': '장점 나열 (차별점)',
        'results':    '기대 효과 (비전)',
    }

    MEDICAL_NOUNS = ['안검하수', '내시경', '거상술', '눈매교정', '매몰법', '절개법']
    MEDICAL_TERM_PATTERN = re.compile(r'[가-힣]+\([一-龥々〇]+,\s*[A-Za-z\s\-]+\)')

    CTA_PATTERNS = [
        re.compile(r'상담을?\s*(추천|받아|예약)'),
        re.compile(r'제이미성형외과.{0,20}(상담|문의)'),
        re.compile(r'편안한\s*마음으로\s*상담'),
    ]

    DISCLAIMER_FRAGMENTS = ['부작용', '출혈', '감염', '의료진과 상담']

    def check(self, content: str) -> List[ValidationIssue]:
        issues: List[ValidationIssue] = []

        # 1. Opening format
        if self.OPENING not in content:
            issues.append(ValidationIssue(
                checker='StructureValidator',
                issue_type='missing_opening',
                severity='high',
                line_number=1,
                matched_text='',
                message=f'표준 인사말이 없습니다: "{self.OPENING}"',
                suggestion=f'글 첫 줄을 "{self.OPENING}"으로 시작하세요.',
            ))

        # 2. 5-step body structure
        detected_steps = []
        for step_key, keywords in self.STEP_KEYWORDS.items():
            for kw in keywords:
                if kw in content:
                    detected_steps.append(step_key)
                    break

        missing_steps = [s for s in self.STEP_KEYWORDS if s not in detected_steps]
        num_missing = len(missing_steps)

        if num_missing >= 2:
            missing_names = ', '.join(self.STEP_NAMES[s] for s in missing_steps)
            issues.append(ValidationIssue(
                checker='StructureValidator',
                issue_type='incomplete_body_structure',
                severity='high',
                line_number=1,
                matched_text='',
                message=f'5단계 본문 구조에서 {num_missing}개 단계가 감지되지 않았습니다: {missing_names}',
                suggestion='본문에 문제 제기, 원인 설명, 해결책 제시, 장점 나열, 기대 효과 섹션을 모두 포함하세요.',
            ))
        elif num_missing == 1:
            missing_names = self.STEP_NAMES[missing_steps[0]]
            issues.append(ValidationIssue(
                checker='StructureValidator',
                issue_type='incomplete_body_structure',
                severity='medium',
                line_number=1,
                matched_text='',
                message=f'5단계 본문 구조에서 1개 단계가 감지되지 않았습니다: {missing_names}',
                suggestion=f'"{missing_names}" 섹션에 해당하는 내용을 추가하세요.',
            ))

        # 3. Medical term format
        has_medical_noun = any(noun in content for noun in self.MEDICAL_NOUNS)
        if has_medical_noun and not self.MEDICAL_TERM_PATTERN.search(content):
            issues.append(ValidationIssue(
                checker='StructureValidator',
                issue_type='missing_medical_term_format',
                severity='medium',
                line_number=1,
                matched_text='',
                message='의학 용어가 포함되어 있으나 한자/영문 병기 형식이 없습니다.',
                suggestion='예: "안검하수(眼瞼下垂, Ptosis)는 ..." 형식으로 첫 의학 용어를 병기하세요.',
            ))

        # 4. Closing CTA (last 20% of content)
        tail_start = max(0, int(len(content) * 0.8))
        tail_content = content[tail_start:]
        has_cta = any(p.search(tail_content) for p in self.CTA_PATTERNS)
        if not has_cta:
            total_lines = content.count('\n') + 1
            issues.append(ValidationIssue(
                checker='StructureValidator',
                issue_type='missing_closing_cta',
                severity='medium',
                line_number=total_lines,
                matched_text='',
                message='글 마지막 20% 구간에 상담 유도(CTA) 문구가 없습니다.',
                suggestion='"지금 바로 제이미성형외과의 [시술명] 상담을 추천드립니다." 형식의 CTA를 추가하세요.',
            ))

        # 5. Required disclaimer (last 30% of content)
        disclaimer_start = max(0, int(len(content) * 0.7))
        disclaimer_content = content[disclaimer_start:]
        found_fragments = [f for f in self.DISCLAIMER_FRAGMENTS if f in disclaimer_content]
        if len(found_fragments) < 2:
            total_lines = content.count('\n') + 1
            issues.append(ValidationIssue(
                checker='StructureValidator',
                issue_type='missing_disclaimer',
                severity='high',
                line_number=total_lines,
                matched_text='',
                message='글 하단에 부작용 고지 면책 문구가 불충분합니다 (부작용, 출혈, 감염, 의료진과 상담 중 2개 이상 필요).',
                suggestion='"개인에 따라 부작용(출혈, 감염, 염증 등)이 있을 수 있으니 사전에 의료진과 상담 후 결정하시기 바랍니다." 문구를 추가하세요.',
            ))

        return issues


class ComplianceChecker:
    """Checks brand compliance rules for Jamie Clinic journal content."""

    PROHIBITED_PATTERNS = {
        'effect_guarantee': {
            'severity': 'critical',
            'patterns': [
                r'100[%％]\s*(?:만족|효과|성공)',
                r'반드시\s+(?:효과|개선|만족)',
                r'완벽한?\s+결과',
                r'보장합니다',
                r'확실한?\s+효과',
                r'틀림없이?\s+(?:효과|개선)',
            ],
            'message': '효과를 보장하는 표현은 의료법 제56조 제2항 제2호 위반입니다.',
            'suggestion': '"개선에 도움을 줄 수 있습니다" 또는 "개인에 따라 결과가 다를 수 있습니다"로 변경하세요.',
        },
        'comparative_superiority': {
            'severity': 'critical',
            'patterns': [
                r'최고의?',
                r'(?:국내|강남|서울|압구정)\s*(?:최고|1위|최초)',
                r'1위',
                r'타\s*병원보다',
                r'다른\s*(?:병원|의원)보다\s*(?:우수|뛰어|좋)',
                r'업계\s*최초',
                r'독보적',
            ],
            'message': '타 의료기관과의 비교 우위 주장은 의료법 제56조 제2항 제4호 위반입니다.',
            'suggestion': '"풍부한 경험을 보유한" 또는 "저희만의 방법으로"와 같은 객관적 표현으로 변경하세요.',
        },
        'safety_guarantee': {
            'severity': 'critical',
            'patterns': [
                r'부작용\s*(?:없|無|제로|zero)',
                r'(?:100[%％]|완전히?|절대)\s*안전',
                r'위험\s*(?:없|無)',
                r'흉터\s*(?:없|無|제로|zero)',
                r'무통(?:증)?(?:\s|$|,)',
            ],
            'message': '안전성을 보장하는 표현은 의료법 제56조 제2항 제7호 위반입니다.',
            'suggestion': '부작용 가능성을 명시하고 "안전한 수술을 위해 최선을 다합니다"로 변경하세요.',
        },
        'patient_testimonial': {
            'severity': 'critical',
            'patterns': [
                r'(?:환자|고객)\s*[가-힣A-Z]+\s*(?:씨|님)의?\s*(?:후기|경험)',
                r'실제\s*(?:환자|고객)\s*(?:후기|리뷰|경험담)',
                r'[""]\s*(?:정말|너무|진짜)\s+(?:만족|좋아요|감사)',
                r'수술\s*후\s*\d+\s*(?:개월|주일|년)\s*(?:만족|경과)',
            ],
            'message': '환자 치료경험담은 의료법 제56조 제2항 제2호 위반입니다.',
            'suggestion': '환자 후기를 제거하고, 일반적인 수술 과정 설명으로 대체하세요.',
        },
        'exaggeration': {
            'severity': 'high',
            'patterns': [
                r'(?:놀라운|대박|극적인)\s*(?:변화|효과|결과)',
                r'마법같은?',
                r'기적적인?',
                r'다시\s*태어나',
                r'완벽\s*변신',
            ],
            'message': '과장된 표현은 의료법 제56조 제2항 제3호 위반 가능성이 있습니다.',
            'suggestion': '"자연스러운 개선", "점진적인 효과"와 같은 절제된 표현으로 변경하세요.',
        },
        'specialty_claim': {
            'severity': 'high',
            'patterns': [
                r'전문\s*병원',
                r'특화\s*(?:병원|클리닉|센터)',
                r'노하우',
            ],
            'message': '전문병원 지정 없이 "전문", "특화" 표현 사용은 과장 광고에 해당합니다.',
            'suggestion': '"중점진료", "집중진료", "풍부한 경험"으로 변경하세요.',
        },
    }

    PROCEDURE_KEYWORDS = [
        '수술', '시술', '이마거상', '쌍꺼풀', '리프팅', '보톡스', '필러',
        '눈매교정', '안검하수', '지방이식', '절개',
    ]

    def check(self, content: str) -> List[ValidationIssue]:
        issues: List[ValidationIssue] = []
        lines = content.split('\n')

        # Check prohibited patterns
        for category, rule in self.PROHIBITED_PATTERNS.items():
            for raw_pat in rule['patterns']:
                compiled = re.compile(raw_pat)
                for m in compiled.finditer(content):
                    line_number = content[:m.start()].count('\n') + 1
                    issues.append(ValidationIssue(
                        checker='ComplianceChecker',
                        issue_type=category,
                        severity=rule['severity'],
                        line_number=line_number,
                        matched_text=m.group(),
                        message=rule['message'],
                        suggestion=rule['suggestion'],
                    ))

        # Check for missing disclaimers when procedure keywords present
        has_procedure = any(kw in content for kw in self.PROCEDURE_KEYWORDS)
        if has_procedure:
            side_effect_terms = ['부작용', '합병증', '붓기', '멍', '염증']
            if not any(t in content for t in side_effect_terms):
                total_lines = len(lines)
                issues.append(ValidationIssue(
                    checker='ComplianceChecker',
                    issue_type='missing_side_effect_notice',
                    severity='high',
                    line_number=total_lines,
                    matched_text='',
                    message='시술/수술 내용이 있으나 부작용 안내가 없습니다.',
                    suggestion='"부작용(출혈, 감염, 염증 등)이 있을 수 있습니다" 형태의 안내를 추가하세요.',
                ))

            individual_variation_pattern = re.compile(r'개인(?:에 따라|마다|별|차이|.{0,30}다를 수|.{0,30}다릅니다)')
            if not individual_variation_pattern.search(content):
                total_lines = len(lines)
                issues.append(ValidationIssue(
                    checker='ComplianceChecker',
                    issue_type='missing_individual_variation_notice',
                    severity='medium',
                    line_number=total_lines,
                    matched_text='',
                    message='시술/수술 내용이 있으나 개인차 고지가 없습니다.',
                    suggestion='"개인에 따라 결과가 다를 수 있습니다" 문구를 추가하세요.',
                ))

        return issues


class SpellingChecker:
    """Checks for common spelling/terminology issues in medical/beauty content."""

    # Format: wrong_term -> (correct_term, category)
    MEDICAL_DICT: Dict[str, Tuple[str, str]] = {
        '쌍커풀':       ('쌍꺼풀',    '눈 성형'),
        '쌍거풀':       ('쌍꺼풀',    '눈 성형'),
        '안겸하수':     ('안검하수',  '눈 성형'),
        '눈꺼플':       ('눈꺼풀',    '눈 성형'),
        '눈커풀':       ('눈꺼풀',    '눈 성형'),
        '네시경':       ('내시경',    '이마 성형'),
        '거상 술':      ('거상술',    '이마 성형'),
        '앤도타인':     ('엔도타인',  '이마 성형'),
        '봉합 사':      ('봉합사',    '이마 성형'),
        '히알루론 산':  ('히알루론산', '필러'),
        '히알류론산':   ('히알루론산', '필러'),
        '지방 이식':    ('지방이식',  '동안 성형'),
        '눈밑지방':     ('눈밑 지방', '눈 성형'),
        '다크 서클':    ('다크서클',  '눈 성형'),
        '팔자주름':     ('팔자 주름', '동안 성형'),
        '절게법':       ('절개법',    '눈 성형'),
        '매몰 법':      ('매몰법',    '눈 성형'),
        '눈썹밑절개':   ('눈썹밑 절개', '이마 성형'),
    }

    def check(self, content: str) -> List[ValidationIssue]:
        issues: List[ValidationIssue] = []
        lines = content.split('\n')

        # Tier 1: built-in medical dictionary scan
        for line_idx, line in enumerate(lines, start=1):
            for wrong, (correct, category) in self.MEDICAL_DICT.items():
                if wrong == correct:
                    continue
                if wrong in line:
                    issues.append(ValidationIssue(
                        checker='SpellingChecker',
                        issue_type='medical_misspelling',
                        severity='medium',
                        line_number=line_idx,
                        matched_text=wrong,
                        message=f'[{category}] 잘못된 표기: "{wrong}" → "{correct}"',
                        suggestion=f'"{wrong}"을 "{correct}"로 수정하세요.',
                    ))

        # Tier 2: optional py-hanspell
        try:
            from hanspell import spell_checker  # type: ignore
            for line_idx, line in enumerate(lines, start=1):
                stripped = line.strip()
                # Skip code blocks, short lines, headings
                if stripped.startswith('```') or stripped.startswith('#') or len(stripped) < 5:
                    continue
                try:
                    result = spell_checker.check(stripped)
                    if result.checked != stripped:
                        issues.append(ValidationIssue(
                            checker='SpellingChecker',
                            issue_type='hanspell_correction',
                            severity='medium',
                            line_number=line_idx,
                            matched_text=stripped[:80],
                            message=f'맞춤법 교정 제안: {result.result}',
                            suggestion=result.checked[:200],
                        ))
                except Exception:
                    pass  # Network or parse error — skip this line
        except ImportError:
            issues.append(ValidationIssue(
                checker='SpellingChecker',
                issue_type='hanspell_unavailable',
                severity='info',
                line_number=0,
                matched_text='',
                message='py-hanspell이 설치되어 있지 않아 맞춤법 자동 검사를 건너뜁니다.',
                suggestion='pip install py-hanspell 을 실행하면 한국어 맞춤법 자동 검사가 활성화됩니다.',
            ))

        return issues


class LinkRecommender:
    """Recommends internal/external links and detects broken or missing ones."""

    PROCEDURE_MAP: Dict[str, Tuple[str, List[str]]] = {
        'double_eyelid':        ('쌍꺼풀 수술',          ['쌍꺼풀', '매몰법', '절개법', '퀵매몰']),
        'ptosis':               ('안검하수 눈매교정',     ['안검하수', '눈매교정', '졸린 눈', '눈매 교정']),
        'under_eye_fat':        ('눈밑 지방 재배치',      ['눈밑 지방', '다크서클', '눈밑지방', '지방 재배치']),
        'dual_epicanthoplasty': ('듀얼 트임',             ['듀얼 트임', '앞트임', '뒤트임', '눈꼬리']),
        'forehead_lift':        ('내시경 이마거상술',     ['이마거상', '이마리프팅', '이마 거상', '이마 리프팅']),
        'brow_lift':            ('내시경 눈썹거상술',     ['눈썹거상', '눈썹리프팅', '눈썹 거상', '눈썹 리프팅']),
        'sub_brow':             ('눈썹밑 피부절개술',     ['눈썹밑', '상안검', '눈꺼풀 처짐']),
        'smas_lifting':         ('스마스 리프팅',         ['스마스', 'SMAS', '안면거상', '페이스리프트']),
        'fat_graft':            ('자가지방 이식',          ['자가지방', '지방이식', '지방 이식', '지방 생착']),
        'hifu':                 ('하이푸 리프팅',          ['하이푸', 'HIFU', '초음파 리프팅']),
        'cheek_lift':           ('앞광대 리프팅',          ['앞광대', '팔자 주름', '팔자주름', '광대 리프팅']),
        'endotine':             ('엔도타인 고정',           ['엔도타인', 'endotine', '3점 고정']),
    }

    def _detect_main_topic(self, lines: List[str]) -> Optional[str]:
        """Scan first 10 lines to find the main procedure topic."""
        opening_text = '\n'.join(lines[:10])
        for topic_key, (display_name, keywords) in self.PROCEDURE_MAP.items():
            for kw in keywords:
                if kw in opening_text:
                    return topic_key
        return None

    def check(self, content: str) -> List[ValidationIssue]:
        issues: List[ValidationIssue] = []
        lines = content.split('\n')

        main_topic = self._detect_main_topic(lines)

        for topic_key, (display_name, keywords) in self.PROCEDURE_MAP.items():
            if topic_key == main_topic:
                continue
            for line_idx, line in enumerate(lines, start=1):
                matched_kw = None
                for kw in keywords:
                    if kw in line:
                        matched_kw = kw
                        break
                if matched_kw:
                    issues.append(ValidationIssue(
                        checker='LinkRecommender',
                        issue_type='internal_link_opportunity',
                        severity='info',
                        line_number=line_idx,
                        matched_text=matched_kw,
                        message=f'"{matched_kw}" 언급 발견 — "{display_name}" 글로의 내부 링크를 고려하세요.',
                        suggestion=f'"{display_name}" 시술 페이지로 연결되는 앵커 텍스트 링크를 추가하세요.',
                    ))
                    break  # one recommendation per topic

        return issues


# ---------------------------------------------------------------------------
# Orchestrator
# ---------------------------------------------------------------------------

VALID_CHECKS = {"structure", "compliance", "spelling", "links"}

_CHECKER_MAP = {
    "structure": StructureValidator,
    "compliance": ComplianceChecker,
    "spelling": SpellingChecker,
    "links": LinkRecommender,
}


class JournalValidator:
    """Orchestrates all validation checks and returns a consolidated report.

    Args:
        checks: List of check names to run. Defaults to all valid checks.
                Valid values: 'structure', 'compliance', 'spelling', 'links'.
    """

    def __init__(self, checks: Optional[List[str]] = None):
        if checks is None:
            self.checks = list(VALID_CHECKS)
        else:
            unknown = set(checks) - VALID_CHECKS
            if unknown:
                raise ValueError(
                    f"Unknown check(s): {', '.join(sorted(unknown))}. "
                    f"Valid options: {', '.join(sorted(VALID_CHECKS))}"
                )
            self.checks = checks

    def validate(self, content: str) -> Dict:
        """Run all enabled checkers against content and return a report dict.

        Returns:
            {
                "is_valid": bool,          # False if any critical or high issue exists
                "total_issues": int,
                "issues_by_severity": {
                    "critical": int,
                    "high": int,
                    "medium": int,
                    "info": int,
                },
                "issues": [dict, ...],     # serialised ValidationIssue dicts
                "summary": str,
            }
        """
        all_issues: List[ValidationIssue] = []

        for check_name in self.checks:
            checker_cls = _CHECKER_MAP[check_name]
            checker = checker_cls()
            issues = checker.check(content)
            all_issues.extend(issues)

        counts: Dict[str, int] = {"critical": 0, "high": 0, "medium": 0, "info": 0}
        for issue in all_issues:
            severity = issue.severity if issue.severity in counts else "info"
            counts[severity] += 1

        is_valid = counts["critical"] == 0 and counts["high"] == 0

        if not all_issues:
            summary = "All checks passed."
        else:
            parts = []
            if counts["critical"]:
                parts.append(f"{counts['critical']} critical")
            if counts["high"]:
                parts.append(f"{counts['high']} high")
            if counts["medium"]:
                parts.append(f"{counts['medium']} medium")
            if counts["info"]:
                parts.append(f"{counts['info']} info")
            summary = f"Found {len(all_issues)} issue(s): {', '.join(parts)}."

        return {
            "is_valid": is_valid,
            "total_issues": len(all_issues),
            "issues_by_severity": counts,
            "issues": [asdict(i) for i in all_issues],
            "summary": summary,
        }


# ---------------------------------------------------------------------------
# CLI
# ---------------------------------------------------------------------------

def _build_parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(
        prog="journal_validator",
        description="Validate a Jamie Journal article against editorial rules.",
    )
    parser.add_argument(
        "--input", "-i",
        required=True,
        metavar="FILE",
        help="Path to the markdown article to validate.",
    )
    parser.add_argument(
        "--check", "-c",
        default="all",
        metavar="CHECKS",
        help=(
            "Comma-separated list of checks to run "
            "(structure, compliance, spelling, links). "
            "Default: all"
        ),
    )
    parser.add_argument(
        "--output", "-o",
        metavar="FILE",
        help="Optional path to write the JSON report.",
    )
    parser.add_argument(
        "--verbose", "-v",
        action="store_true",
        help="Print detailed issue list.",
    )
    return parser


def _print_summary_table(report: Dict, verbose: bool = False) -> None:
    sev = report["issues_by_severity"]
    print("\n=== Journal Validator Report ===")
    print(f"  Status  : {'PASS' if report['is_valid'] else 'FAIL'}")
    print(f"  Total   : {report['total_issues']}")
    print(f"  Critical: {sev['critical']}")
    print(f"  High    : {sev['high']}")
    print(f"  Medium  : {sev['medium']}")
    print(f"  Info    : {sev['info']}")
    print(f"\n  {report['summary']}")

    if verbose and report["issues"]:
        print("\n--- Issues ---")
        for i, issue in enumerate(report["issues"], start=1):
            print(
                f"  [{i}] [{issue['severity'].upper()}] "
                f"Line {issue['line_number']} | {issue['checker']} | "
                f"{issue['message']}"
            )
            if issue.get("suggestion"):
                print(f"       Suggestion: {issue['suggestion']}")
    print()


def main() -> None:
    parser = _build_parser()
    args = parser.parse_args()

    # --- Resolve checks ---
    if args.check.strip().lower() == "all":
        checks = list(VALID_CHECKS)
    else:
        checks = [c.strip() for c in args.check.split(",") if c.strip()]

    # --- Read input file ---
    try:
        with open(args.input, "r", encoding="utf-8") as fh:
            content = fh.read()
    except FileNotFoundError:
        print(f"Error: file not found: {args.input}", file=sys.stderr)
        sys.exit(2)
    except OSError as exc:
        print(f"Error reading file: {exc}", file=sys.stderr)
        sys.exit(2)

    # --- Run validation ---
    try:
        validator = JournalValidator(checks=checks)
    except ValueError as exc:
        print(f"Error: {exc}", file=sys.stderr)
        sys.exit(2)

    report = validator.validate(content)

    # --- Output ---
    _print_summary_table(report, verbose=args.verbose)

    if args.output:
        try:
            with open(args.output, "w", encoding="utf-8") as fh:
                json.dump(report, fh, ensure_ascii=False, indent=2)
            print(f"Report written to: {args.output}")
        except OSError as exc:
            print(f"Error writing report: {exc}", file=sys.stderr)
            sys.exit(2)

    sys.exit(0 if report["is_valid"] else 1)


if __name__ == "__main__":
    main()
