from __future__ import annotations

from collections import defaultdict
from typing import Iterable
import ast
import os
import re
import textwrap

from rich.markup import escape
from rich.panel import Panel

from common.markdown import ReviewRenderPayload, capture_review_payload


def _strip_leading_bullet(text: str) -> str:
    stripped = text.strip()
    for prefix in ("- ", "• ", "* "):
        if stripped.startswith(prefix):
            return stripped[len(prefix):].strip()
    return stripped


def _extract_anchor(text: str) -> tuple[str, str]:
    match = re.search(r"\(anchor:\s*(.*?)\)", text)
    if not match:
        return text.strip(), ""
    anchor = match.group(1).strip()
    cleaned = re.sub(r"\s*\(anchor:\s*.*?\)\s*", "", text).strip()
    return cleaned, anchor


def _parse_risk_line(text: str) -> tuple[str, str, str]:
    cleaned, anchor = _extract_anchor(text)
    if " — " in cleaned:
        left, risk = cleaned.split(" — ", 1)
        left = left.strip()
        if ":" in left:
            # filename:function (line: 86) 형식 처리
            # 먼저 (line: ...) 부분을 제거하고 파싱
            line_match = re.search(r"\s*\(line:\s*(\d+)\)\s*$", left)
            line_number = line_match.group(1) if line_match else None
            left_without_line = re.sub(r"\s*\(line:\s*\d+\)\s*$", "", left).strip()
            
            if ":" in left_without_line:
                path, symbol = left_without_line.split(":", 1)
                symbol = symbol.strip()
                # 빈 문자열이면 "항목"으로 설정
                if not symbol:
                    symbol = "항목"
                # line_number가 있으면 symbol에 다시 추가 (나중에 _find_function_line_number에서 사용)
                if line_number:
                    symbol = f"{symbol} (line: {line_number})"
                return path.strip(), symbol, risk.strip() + (f" (anchor: {anchor})" if anchor else "")
            return left_without_line.strip(), "항목", risk.strip() + (f" (anchor: {anchor})" if anchor else "")
        return left.strip(), "항목", risk.strip() + (f" (anchor: {anchor})" if anchor else "")
    return "", "항목", cleaned.strip() + (f" (anchor: {anchor})" if anchor else "")


def _extract_path_from_text(text: str) -> str:
    """텍스트에서 파일 경로 추출"""
    match = re.search(r"(?P<path>[A-Za-z0-9_./-]+\.(?:py|pyi|js|ts)):", text)
    if match:
        return match.group("path").strip()
    match = re.search(r"(?P<path>[A-Za-z0-9_./-]+\.(?:py|pyi|js|ts))", text)
    if match:
        return match.group("path").strip()
    return "unknown"


def _resolve_existing_path(file_path: str) -> str | None:
    if not file_path or file_path == "unknown":
        return None
    candidate = file_path
    if os.path.isfile(candidate):
        return candidate
    if not os.path.isabs(candidate):
        rel_candidate = os.path.join(os.getcwd(), candidate)
        if os.path.isfile(rel_candidate):
            return rel_candidate
    basename = os.path.basename(candidate)
    if not basename:
        return None
    skip_dirs = {
        ".git",
        ".venv",
        "venv",
        "node_modules",
        "build",
        "dist",
        ".mypy_cache",
        ".pytest_cache",
        ".tox",
        "__pycache__",
    }
    cwd = os.getcwd()
    for root, dirs, files in os.walk(cwd):
        dirs[:] = [d for d in dirs if d not in skip_dirs]
        if basename in files:
            return os.path.join(root, basename)
    return None


def render_review_panel(
    mode: str,
    file_summary: str,
    section_order: Iterable[tuple[str, str]],
    sections: dict[str, list[tuple[str, str]]],
    title: str | None = None,
    notice: str | None = None,
) -> Panel:
    items: list[tuple[str, str, str, str, str]] = []
    label_map = {key: label for key, label in section_order}
    for key, label in section_order:
        for risk, fix in sections.get(key, []):
            risk_line = _strip_leading_bullet(risk)
            if not risk_line:
                continue
            path, symbol, risk_text = _parse_risk_line(risk_line)
            # path가 파일 경로가 아니면 다시 추출 시도
            if path and not path.endswith((".py", ".pyi", ".js", ".ts")):
                # path가 심볼일 수 있으니 risk_text에서 파일 경로 찾기
                extracted_path = _extract_path_from_text(risk_line)
                if extracted_path != "unknown":
                    path = extracted_path
                else:
                    # path를 unknown으로 설정하되 항목은 유지
                    path = "unknown"
            if not path:
                path = "unknown"
            items.append((path, symbol, risk_text, fix.strip(), label_map.get(key, label)))

    if not items:
        capture_review_payload(
            ReviewRenderPayload(
                mode=mode,
                file_summary=file_summary,
                section_order=section_order,
                sections=sections,
                has_findings=False,
                title=title,
                notice=notice,
            )
        )
        lines = ["[No Findings]", "  - 없음"]
        panel_title = title or f"{mode.capitalize()} Review"
        return Panel("\n".join(lines), title=panel_title, border_style="blue")

    capture_review_payload(
        ReviewRenderPayload(
            mode=mode,
            file_summary=file_summary,
            section_order=section_order,
            sections=sections,
            has_findings=True,
            title=title,
            notice=notice,
        )
    )

    file_order: list[str] = []
    grouped: dict[str, list[tuple[str, str, str, str, str]]] = {}
    for entry in items:
        path = entry[0]
        if path not in grouped:
            grouped[path] = []
            file_order.append(path)
        grouped[path].append(entry)

    lines: list[str] = []
    max_per_file = 15

    for path in file_order:
        entries = grouped.get(path, [])
        # 헤더에 사용할 파일 경로 추출
        header_path = None
        if entries:
            # 모든 entries에서 파일 경로 찾기
            for entry_path, entry_symbol, entry_risk, entry_fix, _label in entries:
                # path 필드가 파일 경로인지 확인
                if entry_path and entry_path.endswith((".py", ".pyi", ".js", ".ts")):
                    header_path = entry_path
                    break
                # risk_text에서 파일 경로 찾기
                combined_text = f"{entry_path}:{entry_symbol} — {entry_risk} {entry_fix}"
                match = re.search(r"(?P<path>[A-Za-z0-9_./-]+\.(?:py|pyi|js|ts)):", combined_text)
                if match:
                    header_path = match.group("path")
                    break
                match = re.search(r"(?P<path>[A-Za-z0-9_./-]+\.(?:py|pyi|js|ts))", combined_text)
                if match:
                    header_path = match.group("path")
                    break
        if not header_path or not header_path.endswith((".py", ".pyi", ".js", ".ts")):
            header_path = "unknown"
        header_line = f"[{header_path}] (총 {len(entries)}개)"
        lines.append(f"[yellow]{escape(header_line)}[/]")
        
        # 인터페이스 모드 또는 DB 모드일 때는 카테고리별로 그룹화
        if mode == "interface" or mode == "db":
            # 카테고리별로 그룹화
            category_groups: dict[str, list[tuple[str, str, str, str, str]]] = defaultdict(list)
            for entry in entries:
                _, _, _, _, label = entry
                category_groups[label].append(entry)
            
            # 각 카테고리별로 출력
            for category, category_entries in category_groups.items():
                # 각 카테고리 내에서 제안(fix_line)만으로 그룹화
                # 같은 해결법을 가진 항목들을 묶기 위해 제안만 비교
                message_groups: dict[str, list[tuple[str, str, str, str, str]]] = defaultdict(list)
                for entry in category_entries:
                    _, _, risk_text, fix_line, _ = entry
                    # 제안 정규화 (anchor 제거, Fix: 접두사 제거, 공백 정리)
                    clean_fix = fix_line or ""
                    if clean_fix:
                        clean_fix = re.sub(r"\s*\(anchor:\s*.*?\)\s*", "", clean_fix).strip()
                        if clean_fix.startswith("Fix:"):
                            clean_fix = clean_fix[4:].strip()
                        # 괄호 안의 내용 제거 (예: "(Fix: ...)" 부분)
                        clean_fix = re.sub(r"\s*\(Fix:\s*[^)]+\)\s*", "", clean_fix).strip()
                        
                        # 함수명/클래스명 제거 (공통 내용만 남기기)
                        # "xxx 함수 정의에서 ..." -> "함수 정의에서 ..."
                        clean_fix = re.sub(r"\w+\s+함수\s+", "함수 ", clean_fix)
                        # "xxx 클래스의 ..." -> "클래스의 ..."
                        clean_fix = re.sub(r"\w+\s+클래스\s*의\s+", "클래스의 ", clean_fix)
                        # "xxx의 ..." -> "..."
                        clean_fix = re.sub(r"\w+\s*의\s+", "", clean_fix)
                        # "def xxx(...) 줄에서 ..." -> "줄에서 ..."
                        clean_fix = re.sub(r"def\s+\w+[^줄]*줄에서", "줄에서", clean_fix)
                        # "__init__ 메서드" -> "메서드"
                        clean_fix = re.sub(r"__init__\s+메서드", "메서드", clean_fix)
                        
                        # 구체적인 위치 정보 제거 (핵심 액션만 남기기)
                        # "if 문 뒤에", "반환 타입(int) 뒤에", "다음에" 등 위치 정보 제거
                        clean_fix = re.sub(r"(if\s+\w+|반환\s+타입\([^)]+\)|파라미터\s+목록|함수\s+정의|메서드\s+정의)\s*(뒤에|다음에|에서|에)", "", clean_fix)
                        # "줄에서" 제거
                        clean_fix = re.sub(r"\s*줄에서\s*", " ", clean_fix)
                        # 핵심 액션만 남기기 (예: "콜론(:)을 추가", "타입힌트를 추가" 등)
                        
                        # 문장 끝의 어미 정규화 (추가하라/추가한다 -> 추가)
                        clean_fix = re.sub(r"([가-힣]+)(하라|한다|하세요|하시오|해주세요|해주시오)\s*\.?\s*$", r"\1", clean_fix).strip()
                        # 공백 정리
                        clean_fix = re.sub(r"\s+", " ", clean_fix).strip()
                    # 정규화된 제안을 키로 그룹화
                    key = clean_fix
                    message_groups[key].append(entry)
                
                # 각 메시지 그룹별로 출력
                for clean_fix_key, group_entries in message_groups.items():
                    # 첫 번째 entry에서 원본 risk_text와 fix_line 가져오기
                    _, _, risk_text, fix_line, _ = group_entries[0]
                    # 해당 그룹의 라인 번호 수집
                    line_numbers = []
                    file_path_to_use = header_path
                    
                    for entry_path, symbol, _, _, _ in group_entries:
                        # symbol에서 라인 번호 추출 시도 (예: "function_name (line: 123)" 형식)
                        line_number = None
                        if symbol:
                            # (line: 123) 형식에서 라인 번호 추출
                            line_match = re.search(r"\(line:\s*(\d+)\)", symbol)
                            if line_match:
                                line_number = int(line_match.group(1))
                            else:
                                # symbol에서 함수명 추출하여 라인 번호 찾기
                                symbol_text = symbol or "항목"
                                # 심볼에서 카테고리 라벨 제거
                                symbol_text = re.sub(r"^[^:)]+[):]\s*", "", symbol_text).strip()
                                # (line: ...) 부분 제거
                                symbol_text = re.sub(r"\s*\(line:\s*\d+\)\s*", "", symbol_text).strip()
                                
                                if symbol_text and file_path_to_use and file_path_to_use.endswith((".py", ".pyi")):
                                    line_number = _find_function_line_number(file_path_to_use, symbol_text)
                        
                        if line_number:
                            line_numbers.append(line_number)
                    
                    line_numbers = sorted(set(line_numbers))  # 중복 제거 및 정렬
                    
                    # 카테고리 이름 출력 (파란색)
                    lines.append(f"  [bold cyan]{category}[/bold cyan]")
                    
                    # 라인 번호 출력
                    if line_numbers:
                        line_str = ",".join(map(str, line_numbers))
                        lines.append(f"  line {line_str}")
                    else:
                        lines.append("  line (unknown)")
                    
                    # 메시지와 제안 출력 (첫 번째 entry의 것만 사용)
                    clean_risk = re.sub(r"\s*\(anchor:\s*.*?\)\s*", "", risk_text).strip()
                    if clean_risk:
                        # 메시지에서 특정 함수명/클래스명 제거 (공통 내용만 남기기)
                        # "함수 'xxx'의 ..." -> "함수의 ..."
                        clean_risk = re.sub(r"함수\s+['\"]([^'\"]+)['\"]\s*의", "함수의", clean_risk)
                        # "클래스 'xxx'의 ..." -> "클래스의 ..."
                        clean_risk = re.sub(r"클래스\s+['\"]([^'\"]+)['\"]\s*의", "클래스의", clean_risk)
                        # "'xxx' 함수의 ..." -> "함수의 ..."
                        clean_risk = re.sub(r"['\"]([^'\"]+)['\"]\s*함수의", "함수의", clean_risk)
                        # "'xxx' 클래스의 ..." -> "클래스의 ..."
                        clean_risk = re.sub(r"['\"]([^'\"]+)['\"]\s*클래스의", "클래스의", clean_risk)
                        # "'xxx' 메서드의 ..." -> "메서드의 ..."
                        clean_risk = re.sub(r"['\"]([^'\"]+)['\"]\s*메서드의", "메서드의", clean_risk)
                        # "'xxx' 함수" -> "함수"
                        clean_risk = re.sub(r"['\"]([^'\"]+)['\"]\s*함수", "함수", clean_risk)
                        # "'xxx' 클래스" -> "클래스"
                        clean_risk = re.sub(r"['\"]([^'\"]+)['\"]\s*클래스", "클래스", clean_risk)
                        # 공백 정리
                        clean_risk = re.sub(r"\s+", " ", clean_risk).strip()
                        lines.append(f"  {_truncate_text(clean_risk)}")
                    if fix_line:
                        clean_fix = re.sub(r"\s*\(anchor:\s*.*?\)\s*", "", fix_line).strip()
                        if clean_fix.startswith("Fix:"):
                            clean_fix = clean_fix[4:].strip()
                        # 괄호 안의 내용 제거 (예: "(Fix: ...)" 부분)
                        clean_fix = re.sub(r"\s*\(Fix:\s*[^)]+\)\s*", "", clean_fix).strip()
                        if clean_fix:
                            lines.append(f"[green]{_truncate_text(f'  > {clean_fix}')}[/green]")
                    
                    lines.append("")  # 그룹 간 빈 줄
        else:
            # 인터페이스 모드가 아닐 때는 기존 로직 유지
            shown = 0
            total_to_show = min(len(entries), max_per_file)
            for path, symbol, risk_text, fix_line, label in entries:
                if shown >= max_per_file:
                    break
                symbol_text = symbol or "항목"
                # 심볼에서 (line: ...) 부분을 먼저 추출하고 제거
                line_match = re.search(r"\s*\(line:\s*(\d+)\)\s*$", symbol_text)
                line_number_from_symbol = line_match.group(1) if line_match else None
                symbol_text = re.sub(r"\s*\(line:\s*\d+\)\s*$", "", symbol_text).strip()
                
                # 심볼에서 카테고리 라벨 제거 (예: "인증·인가) auth_login" -> "auth_login")
                symbol_text = re.sub(r"^[^:)]+[):]\s*", "", symbol_text).strip()
                
                # 심볼이 코드 스니펫처럼 보이면 (특수문자 포함, 너무 긴 경우) "항목"으로 대체
                if symbol_text and (re.search(r'[{}()\[\]":,;]', symbol_text) or len(symbol_text) > 30):
                    # 코드 스니펫에서 변수명/함수명 추출 시도
                    var_match = re.search(r'\b([A-Z_][A-Z0-9_]*)\s*=', symbol_text)
                    if var_match:
                        symbol_text = var_match.group(1)
                    else:
                        func_match = re.search(r'\b([A-Za-z_][A-Za-z0-9_]*)\s*\(', symbol_text)
                        if func_match:
                            symbol_text = func_match.group(1)
                        else:
                            # 유효한 식별자 추출
                            identifier_match = re.search(r'\b([A-Za-z_][A-Za-z0-9_]{2,})\b', symbol_text)
                            if identifier_match:
                                candidate = identifier_match.group(1)
                                if candidate not in ('def', 'class', 'if', 'for', 'while', 'return', 'import', 'from', 'as', 'in', 'is', 'and', 'or', 'not', 'True', 'False', 'None'):
                                    symbol_text = candidate
                                else:
                                    symbol_text = "항목"
                            else:
                                symbol_text = "항목"
                
                # 함수명/변수명에 줄 번호 추가: 함수명 (line 164) 형식
                file_path_to_use = header_path if header_path and header_path != "unknown" else path
                resolved_path = _resolve_existing_path(file_path_to_use) or file_path_to_use
                
                # line_number_from_symbol이 있으면 무조건 추가 (항목인 경우에도)
                if line_number_from_symbol:
                    symbol_text = f"{symbol_text} (line {line_number_from_symbol})"
                elif resolved_path and resolved_path.endswith((".py", ".pyi")):
                    # symbol에 line: 정보가 없으면 파일에서 찾기 시도
                    # "항목"인 경우에도 risk_text에서 키워드를 찾아 라인 번호를 찾으려고 시도
                    if symbol_text == "항목":
                        # risk_text에서 코드 스니펫이나 함수명 추출 시도
                        risk_clean = re.sub(r"\s*\(anchor:\s*.*?\)\s*", "", risk_text).strip()
                        
                        # security 모드에서만 취약점 유형에 따라 관련 키워드 찾기
                        candidate = None
                        if mode == "security":
                            # SQL 인젝션 관련 키워드
                            if 'SQL' in risk_clean or 'sql' in risk_clean.lower() or '인젝션' in risk_clean:
                                sql_keywords = ['execute', 'query', 'cursor', 'fetchone', 'fetchall', 'fetchmany']
                                for keyword in sql_keywords:
                                    if keyword in risk_clean.lower():
                                        candidate = keyword
                                        break
                            # 명령 주입 관련 키워드
                            elif '명령' in risk_clean or 'command' in risk_clean.lower() or '주입' in risk_clean:
                                cmd_keywords = ['check_output', 'Popen', 'system', 'call', 'run', 'exec']
                                for keyword in cmd_keywords:
                                    if keyword in risk_clean.lower():
                                        candidate = keyword
                                        break
                            # 경로 탐색 관련 키워드
                            elif '경로' in risk_clean or 'path' in risk_clean.lower() or '파일' in risk_clean:
                                path_keywords = ['open', 'read', 'write', 'file', 'path']
                                for keyword in path_keywords:
                                    if keyword in risk_clean.lower():
                                        candidate = keyword
                                        break
                            # XSS 관련 키워드
                            elif 'XSS' in risk_clean or 'xss' in risk_clean.lower():
                                xss_keywords = ['render', 'html', 'Response', 'escape']
                                for keyword in xss_keywords:
                                    if keyword in risk_clean.lower():
                                        candidate = keyword
                                        break
                            # SSRF 관련 키워드
                            elif 'SSRF' in risk_clean or 'ssrf' in risk_clean.lower():
                                ssrf_keywords = ['requests', 'get', 'post', 'urlopen', 'fetch']
                                for keyword in ssrf_keywords:
                                    if keyword in risk_clean.lower():
                                        candidate = keyword
                                        break
                        
                        # 위에서 찾지 못했으면 일반 식별자 추출 (모든 모드에서)
                        if not candidate:
                            identifier_match = re.search(r'\b([A-Za-z_][A-Za-z0-9_]{2,})\b', risk_clean)
                            if identifier_match:
                                candidate = identifier_match.group(1)
                                if candidate in ('def', 'class', 'if', 'for', 'while', 'return', 'import', 'from', 'as', 'in', 'is', 'and', 'or', 'not', 'True', 'False', 'None', '취약점', '인젝션', '노출', '하드코딩된', '사용되었습니다', '존재합니다'):
                                    candidate = None
                        
                        if candidate:
                            line_number = _find_function_line_number(resolved_path, candidate)
                            if line_number:
                                symbol_text = f"{candidate} (line {line_number})"
                            else:
                                # 라인 번호를 찾지 못해도 함수명은 표시
                                symbol_text = candidate
                    elif symbol_text and symbol_text != "항목":
                        line_number = _find_function_line_number(resolved_path, symbol_text)
                        if line_number:
                            symbol_text = f"{symbol_text} (line {line_number})"
                
                clean_risk = re.sub(r"\s*\(anchor:\s*.*?\)\s*", "", risk_text).strip()
                lines.append(f"  {symbol_text}")
                lines.append(_truncate_text(f"  - {clean_risk}"))
                if fix_line:
                    clean_fix = re.sub(r"\s*\(anchor:\s*.*?\)\s*", "", fix_line).strip()
                    if clean_fix.startswith("Fix:"):
                        clean_fix = clean_fix[4:].strip()
                    lines.append(f"[green]{_truncate_text(f'  > {clean_fix}')}[/green]")
                # 항목 끝에 빈 줄 추가 (마지막 항목 제외)
                shown += 1
                if shown < total_to_show:
                    lines.append("")
            if len(entries) > shown:
                lines.append(f"  (... +{len(entries) - shown} more)")
        
        lines.append("")

    panel_title = title or f"{mode.capitalize()} Review"
    return Panel("\n".join(lines).rstrip(), title=panel_title, border_style="blue")


def _truncate_text(text: str, width: int = 100, max_lines: int = 4) -> str:
    """텍스트를 그대로 반환하여, 터미널/리치 패널이 자연스럽게 줄바꿈하게 둔다.

    이전에는 인위적으로 줄을 자르거나 \"...\"를 붙였지만,
    이제는 내용 자체는 안 자르고 출력 쪽에서 너비에 맞게 알아서 줄바꿈되도록 한다.
    """
    return text


def _find_function_line_number(file_path: str, symbol_name: str) -> int | None:
    """파일에서 함수, 클래스, 변수, 또는 키워드/구문의 줄 번호를 찾기
    
    Args:
        file_path: 파일 경로
        symbol_name: 심볼명 (함수명, 클래스명, 변수명, 또는 키워드)
        
    Returns:
        심볼의 줄 번호 (없으면 None)
    """
    if not file_path or not symbol_name:
        return None
    
    if not symbol_name or symbol_name == "항목":
        return None
    
    # 파일 경로가 상대 경로일 수 있으므로 절대 경로로 변환 시도
    if not os.path.isabs(file_path):
        possible_paths = [
            file_path,
            os.path.join(os.getcwd(), file_path),
        ]
    else:
        possible_paths = [file_path]
    
    for path_str in possible_paths:
        if not os.path.exists(path_str):
            continue
        
        try:
            with open(path_str, "r", encoding="utf-8", errors="replace") as f:
                lines = f.readlines()
                source = "".join(lines)
            
            # 1. 변수 할당 패턴 찾기 (예: API_KEY = "...")
            var_pattern = re.compile(rf"^\s*{re.escape(symbol_name)}\s*=", re.MULTILINE)
            for i, line in enumerate(lines, 1):
                if var_pattern.search(line):
                    return i
            
            # 2. 함수 호출 패턴 찾기 (예: cursor.execute(...), subprocess.check_output(...))
            # symbol_name이 함수명인 경우 (예: execute, check_output, system, open)
            func_call_pattern = re.compile(rf"\b{re.escape(symbol_name)}\s*\(", re.MULTILINE)
            for i, line in enumerate(lines, 1):
                if func_call_pattern.search(line):
                    return i
            
            # 3. 함수 정의 찾기 (예: def function_name(...))
            try:
                tree = ast.parse(source, filename=path_str)
            except SyntaxError:
                # AST 파싱 실패 시 소스 코드에서 직접 검색
                for i, line in enumerate(lines, 1):
                    stripped = line.strip()
                    # try, if, return 등의 키워드가 줄 시작에 있는지 확인
                    if stripped.startswith(symbol_name + " ") or stripped.startswith(symbol_name + ":"):
                        return i
                return None
            
            # AST를 순회하며 함수, 클래스 찾기
            for node in ast.walk(tree):
                if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
                    if node.name == symbol_name:
                        return node.lineno
                elif isinstance(node, ast.ClassDef):
                    if node.name == symbol_name:
                        return node.lineno
                # 변수 할당 찾기 (AST 레벨)
                elif isinstance(node, ast.Assign):
                    for target in node.targets:
                        if isinstance(target, ast.Name) and target.id == symbol_name:
                            return node.lineno
                        elif isinstance(target, ast.Attribute) and target.attr == symbol_name:
                            return node.lineno
            
            # 4. 키워드/구문 fallback
            for i, line in enumerate(lines, 1):
                stripped = line.strip()
                # try, if, return 등의 키워드가 줄 시작에 있는지 확인
                if stripped.startswith(symbol_name + " ") or stripped.startswith(symbol_name + ":"):
                    return i
                # 부분 매칭 (예: "if username == "admin""에서 "username" 찾기)
                if re.search(rf"\b{re.escape(symbol_name)}\b", line):
                    return i
        except Exception:
            continue
    
    return None
