"""
Modal handler: after a click, repeatedly check for modals using OpenAI Vision,
and if a save-type modal is open, click the Save button via Rekognition tiler;
otherwise click Cancel. Loop until no modals are detected or a safety limit.

This is designed to be called from the analyzer after actions that may open
confirmation dialogs.
"""

from __future__ import annotations

from typing import Optional, Dict, List, Tuple
import time
import json
from pathlib import Path
import re
import shutil

import pyautogui
from PIL import Image
import boto3

from .rekognition_tiler import find_text_coordinates_tiled
from .openai_analyzer import OpenAIAnalyzer
from .constants import AWS_REGION


SAVE_KEYWORDS = [
    "save", "confirm", "ok", "yes", "send", "submit", "apply"
]
CANCEL_KEYWORDS = [
    "cancel", "close", "dismiss", "no", "abort"
]

# Heuristics to detect summaries that explicitly indicate no modal is present
NO_MODAL_KEYWORDS = [
    "no modal", "no dialog", "no modal visible", "no modal dialog",
    "none visible", "no popup", "no pop-up", "no modal is visible",
]


def _contains_any(text: str, keywords: List[str]) -> bool:
    low = (text or "").lower()
    return any(k in low for k in keywords)


def _summary_indicates_no_modal(text: str) -> bool:
    return _contains_any(text, NO_MODAL_KEYWORDS)


def _analyze_for_modal(analyzer: OpenAIAnalyzer, screenshot_path: str, debug_dir: Optional[Path] = None) -> Dict:
    """Use OpenAI to determine if there is a modal and which exact button to click.

    Returns dict:
      {
        "has_modal": bool,
        "type": "save"|"cancel"|"none",
        "summary": str,
        "button_text": str|None,          # exact label to click (case-sensitive)
        "alt_texts": [str, ...]|None      # other plausible exact labels
      }
    """
    prompt = (
        "You are checking if a modal dialog is visible in the screenshot.\n"
        "- If a modal is open and there is a clear Save/Confirm/Submit action that finalizes the user's intended operation, set type=save.\n"
        "- Prefer SAVE (e.g., 'Save', 'OK', 'Yes', 'Confirm', 'Submit', 'Apply') over CANCEL when both are present.\n"
        "- Prefer Cancel over 'Mark as sent' when both are present.\n"
        "- Only set type=cancel when it is clearly a dismiss/abort action or saving would be unsafe.\n"
        "- If no modal is visible, set type=none.\n"
        "Return ONLY strict JSON with keys: has_modal, type, summary, button_text, alt_texts.\n"
        "- button_text: the EXACT button label to click, as shown on-screen (case and spacing preserved).\n"
        "  Return the exact label to press for the chosen action (save/confirm or cancel/dismiss).\n"
        "- alt_texts: array of other plausible EXACT labels (may be empty).\n"
        "Example: {\n"
        "  \"has_modal\": true,\n"
        "  \"type\": \"save\",\n"
        "  \"summary\": \"Dialog with Save and Cancel\",\n"
        "  \"button_text\": \"Save\",\n"
        "  \"alt_texts\": [\"OK\", \"Confirm\"]\n"
        "}"
    )
    # Call into whichever component provides the vision call: prefer analyzer if available,
    # else delegate to app or screenshot manager.
    result = None
    try:
        if hasattr(analyzer, 'analyze_screenshot_with_openai') and callable(getattr(analyzer, 'analyze_screenshot_with_openai')):
            try:
                analyzer.openai_logger.info("[Modal] Using analyzer.analyze_screenshot_with_openai for modal detection")
            except Exception:
                pass
            result = analyzer.analyze_screenshot_with_openai(screenshot_path, prompt)  # type: ignore[attr-defined]
        elif getattr(analyzer, 'app', None) is not None:
            app = getattr(analyzer, 'app')
            if hasattr(app, 'analyze_screenshot_with_openai') and callable(getattr(app, 'analyze_screenshot_with_openai')):
                try:
                    analyzer.openai_logger.info("[Modal] Using app.analyze_screenshot_with_openai for modal detection")
                except Exception:
                    pass
                result = app.analyze_screenshot_with_openai(screenshot_path, prompt)
            elif hasattr(app, 'screenshot_manager') and hasattr(app.screenshot_manager, 'analyze_screenshot_with_openai'):
                try:
                    analyzer.openai_logger.info("[Modal] Using screenshot_manager.analyze_screenshot_with_openai for modal detection")
                except Exception:
                    pass
                result = app.screenshot_manager.analyze_screenshot_with_openai(screenshot_path, prompt)
    except Exception as e:
        try:
            analyzer.openai_logger.exception(f"[Modal] Error calling analyze_screenshot_with_openai: {e}")
        except Exception:
            pass
        result = None
    if result is None:
        # Fail-safe: no response; return default none
        data_fail = {"has_modal": False, "type": "none", "summary": "vision_call_failed", "button_text": None, "alt_texts": []}
        # Write minimal debug artifacts if requested
        try:
            if debug_dir:
                debug_dir.mkdir(parents=True, exist_ok=True)
                # Save prompt and an input copy for traceability
                (debug_dir / 'prompt.txt').write_text(prompt, encoding='utf-8')
                try:
                    shutil.copyfile(screenshot_path, str(debug_dir / 'input.png'))
                except Exception:
                    pass
                (debug_dir / 'response_raw.txt').write_text('None', encoding='utf-8')
                (debug_dir / 'parsed.json').write_text(json.dumps(data_fail, ensure_ascii=False, indent=2), encoding='utf-8')
                # Write meta for verification
                try:
                    import hashlib, os
                    meta = {
                        'screenshot_path': str(screenshot_path),
                        'size_bytes': os.path.getsize(screenshot_path) if os.path.exists(screenshot_path) else None,
                        'mtime': os.path.getmtime(screenshot_path) if os.path.exists(screenshot_path) else None,
                        'sha1': hashlib.sha1(open(screenshot_path, 'rb').read()).hexdigest() if os.path.exists(screenshot_path) else None,
                    }
                    (debug_dir / 'meta.json').write_text(json.dumps(meta, ensure_ascii=False, indent=2), encoding='utf-8')
                except Exception:
                    pass
        except Exception:
            pass
        return data_fail
    data: Dict = {"has_modal": False, "type": "none", "summary": "", "button_text": None, "alt_texts": []}
    try:
        # Try to parse JSON response; else fallback to keyword heuristics
        parsed: Optional[Dict] = None
        if isinstance(result, str):
            raw = result.strip()
            # Strip code fences like ```json ... ``` or ``` ... ```
            if raw.startswith("```"):
                raw = re.sub(r"^```[a-zA-Z]*\n?", "", raw)
                raw = re.sub(r"\n?```$", "", raw)
                raw = raw.strip()
            if raw.startswith("{"):
                parsed = json.loads(raw)
            else:
                # Extract first JSON object heuristically
                start = raw.find('{')
                end = raw.rfind('}')
                if start != -1 and end != -1 and end > start:
                    snippet = raw[start:end+1]
                    parsed = json.loads(snippet)
        if isinstance(parsed, dict):
            data = parsed
        else:
            data = {"has_modal": False, "type": "none", "summary": str(result), "button_text": None, "alt_texts": []}
    except Exception as e:
        data = {"has_modal": False, "type": "none", "summary": str(result), "button_text": None, "alt_texts": []}
        # Save parse error detail if debugging
        try:
            if debug_dir:
                (debug_dir / 'parse_error.txt').write_text(f"{e}", encoding='utf-8')
        except Exception:
            pass
    # Persist debug artifacts if a debug directory is provided
    try:
        if debug_dir:
            debug_dir.mkdir(parents=True, exist_ok=True)
            try:
                shutil.copyfile(screenshot_path, str(debug_dir / 'input.png'))
            except Exception:
                pass
            try:
                (debug_dir / 'prompt.txt').write_text(prompt, encoding='utf-8')
            except Exception:
                pass
            try:
                (debug_dir / 'response_raw.txt').write_text(str(result), encoding='utf-8')
            except Exception:
                pass
            try:
                (debug_dir / 'parsed.json').write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding='utf-8')
            except Exception:
                pass
            # Also write meta to confirm the specific image used this iteration
            try:
                import hashlib, os
                meta = {
                    'screenshot_path': str(screenshot_path),
                    'size_bytes': os.path.getsize(screenshot_path) if os.path.exists(screenshot_path) else None,
                    'mtime': os.path.getmtime(screenshot_path) if os.path.exists(screenshot_path) else None,
                    'sha1': hashlib.sha1(open(screenshot_path, 'rb').read()).hexdigest() if os.path.exists(screenshot_path) else None,
                }
                (debug_dir / 'meta.json').write_text(json.dumps(meta, ensure_ascii=False, indent=2), encoding='utf-8')
            except Exception:
                pass
    except Exception:
        pass
    # Heuristic normalization using parsed summary/text
    txt = str(data.get("summary") or "")
    # If summary clearly says there is no modal, override to none
    if _summary_indicates_no_modal(txt):
        data = {"has_modal": False, "type": "none", "summary": txt, "button_text": None, "alt_texts": []}
    elif not data.get("has_modal"):
        # Only if has_modal is False and summary implies action, infer type
        if _contains_any(txt, SAVE_KEYWORDS):
            data = {"has_modal": True, "type": "save", "summary": txt, "button_text": None, "alt_texts": []}
        elif _contains_any(txt, CANCEL_KEYWORDS):
            data = {"has_modal": True, "type": "cancel", "summary": txt, "button_text": None, "alt_texts": []}
    return data


def _click_by_rekognition_tiler(
    analyzer: OpenAIAnalyzer,
    screenshot_path: str,
    text_query: str,
    debug_parent: Optional[Path] = None,
    allow_fallback: bool = True,
) -> bool:
    try:
        img = Image.open(screenshot_path).convert('RGB')
        rk = boto3.client('rekognition', region_name=AWS_REGION)
        # Place Rekognition artifacts under the current iteration's debug folder when provided
        base_dir = debug_parent if debug_parent is not None else Path(analyzer._get_debug_dir())
        # Ensure base_dir exists and create a unique subdirectory for this Rekognition run
        try:
            Path(base_dir).mkdir(parents=True, exist_ok=True)
        except Exception:
            pass
        debug_dir_path = Path(base_dir) / f"rekognition_{int(time.time()*1000)}"
        try:
            debug_dir_path.mkdir(parents=True, exist_ok=True)
        except Exception:
            pass
        debug_dir = str(debug_dir_path)
        try:
            analyzer.openai_logger.info(
                f"[Modal] Rekognition search start: query='{text_query}', screenshot='{screenshot_path}', debug_dir='{debug_dir}'"
            )
        except Exception:
            pass
        # Build a strict, case-insensitive regex that matches the full label (not substrings like 'as')
        import re as _re
        raw = str(text_query or '').strip()
        if not raw:
            return False
        # Guard against ultra-short/common stopwords to avoid false positives
        _allow_short = {"ok", "yes", "no"}
        _stopwords = {"as", "of", "to", "in", "on", "by", "at"}
        if len(raw) < 3 and raw.lower() not in _allow_short:
            try:
                analyzer.openai_logger.info(f"[Modal] Skipping too-short label '{raw}' to avoid false positives")
            except Exception:
                pass
            return False
        if raw.lower() in _stopwords:
            try:
                analyzer.openai_logger.info(f"[Modal] Skipping stopword label '{raw}' to avoid false positives")
            except Exception:
                pass
            return False

        # Build phrase regex with strict word boundaries between all tokens
        tokens = [_re.escape(t) for t in raw.split() if t]
        if not tokens:
            return False
        if len(tokens) == 1:
            phrase = fr"(?i)\b{tokens[0]}\b"
        else:
            phrase = fr"(?i)\b" + "\\b\\s+\\b".join(tokens) + fr"\b"

        # Use a 1x8 grid for 'Download' to better isolate the label in text-dense screens; default otherwise
        is_download = raw.lower() == 'download'
        coords = find_text_coordinates_tiled(
            rk,
            img,
            query=phrase,
            is_regex=True,
            upscale=2.0,
            overlap_frac=0.10,
            debug_dir=debug_dir,
            require_include=not allow_fallback,
            cols=1 if is_download else 1,
            rows=8 if is_download else 5,
        )
        # Fallback: try without common stopwords in the middle if not found
        if (not isinstance(coords, (list, tuple)) or len(coords) != 2) and len(tokens) > 1:
            tokens_no_stop = [t for t in tokens if _re.sub(r"\\\\", "", t).lower() not in _stopwords]
            if tokens_no_stop:
                if len(tokens_no_stop) == 1:
                    phrase2 = fr"(?i)\b{tokens_no_stop[0]}\b"
                else:
                    phrase2 = fr"(?i)\b" + "\\b\\s+\\b".join(tokens_no_stop) + fr"\b"
                coords = find_text_coordinates_tiled(
                    rk,
                    img,
                    query=phrase2,
                    is_regex=True,
                    upscale=2.0,
                    overlap_frac=0.10,
                    debug_dir=debug_dir,
                    require_include=not allow_fallback,
                    cols=1 if is_download else 1,
                    rows=7 if is_download else 5,
                )
        if isinstance(coords, (list, tuple)) and len(coords) == 2:
            x, y = int(coords[0]), int(coords[1])
            try:
                analyzer.openai_logger.info(
                    f"[Modal] Rekognition match for '{text_query}' at coords=({x}, {y})"
                )
            except Exception:
                pass
            # Expose last Rekognition match coordinates for optional reuse by callers
            try:
                setattr(analyzer, "_last_rekognition_coords", (x, y))
            except Exception:
                pass
            restore = analyzer._temporarily_hide_app()
            try:
                pyautogui.moveTo(x, y, duration=0.3)
                pyautogui.click(x, y)
            finally:
                try:
                    restore()
                except Exception:
                    pass
            return True
        # If we did not get an include match and fallback is not allowed, do not click
        if not allow_fallback:
            try:
                analyzer.openai_logger.info(
                    f"[Modal] Rekognition: no strict match for '{text_query}' (fallback disabled)"
                )
            except Exception:
                pass
            return False
        try:
            analyzer.openai_logger.info(
                f"[Modal] Rekognition: no match for '{text_query}'"
            )
        except Exception:
            pass
        return False
    except Exception as e:
        try:
            analyzer.openai_logger.exception(f"[Modal] Rekognition error for '{text_query}': {e}")
        except Exception:
            pass
        return False


def _click_by_openai_search(
    analyzer: OpenAIAnalyzer,
    screenshot_path: str,
    text_query: str,
    debug_parent: Optional[Path] = None,
) -> bool:
    """Fallback using OpenAI analyzer's coordinate finder to click a label."""
    try:
        try:
            analyzer.openai_logger.info(f"[Modal] OpenAI search fallback for '{text_query}'")
        except Exception:
            pass
        # Use analyzer's search (context-aware vision) to find coordinates
        coords = None
        try:
            coords = analyzer.find_text_coordinates(screenshot_path, text_query)
        except Exception:
            coords = None
        if isinstance(coords, (list, tuple)) and len(coords) == 2:
            x, y = int(coords[0]), int(coords[1])
            restore = analyzer._temporarily_hide_app()
            try:
                pyautogui.moveTo(x, y, duration=0.3)
                pyautogui.click(x, y)
            finally:
                try:
                    restore()
                except Exception:
                    pass
            return True
        return False
    except Exception:
        return False


def _click_label_prefer_rekognition_then_openai(
    analyzer: OpenAIAnalyzer,
    screenshot_path: str,
    text_query: str,
    debug_parent: Optional[Path] = None,
) -> bool:
    """Try Rekognition first; on failure, use OpenAI search to click the label."""
    if _click_by_rekognition_tiler(analyzer, screenshot_path, text_query, debug_parent=debug_parent):
        return True
    return _click_by_openai_search(analyzer, screenshot_path, text_query, debug_parent=debug_parent)

def handle_modals_until_clear(analyzer: OpenAIAnalyzer, max_loops: int = 5, wait_between: float = 5.0) -> Dict:
    """Loop: capture screen, ask OpenAI if a modal is open, then click Save or Cancel via Rekognition.

    Returns a dict with the actions taken.
    """
    actions: List[Dict] = []
    cleared = False
    # Create a dedicated debug folder for this modal session
    try:
        session_dir = Path(analyzer._get_debug_dir()) / f"modal_{int(time.time()*1000)}"
        session_dir.mkdir(parents=True, exist_ok=True)
        try:
            analyzer.openai_logger.info(f"[Modal] Debug session dir: {session_dir}")
        except Exception:
            pass
    except Exception:
        session_dir = None  # type: ignore
    for idx in range(max_loops):
        # Wait before the next check (except the first)
        if idx > 0:
            try:
                time.sleep(wait_between)
            except Exception:
                pass
        # Capture
        screenshot_path = analyzer.capture_full_resolution_screenshot()
        if not screenshot_path:
            break
        # Analyze
        try:
            analyzer.openai_logger.info(f"[Modal] Iteration {idx+1}: analyzing screenshot {screenshot_path}")
        except Exception:
            pass
        # Iteration-specific debug dir
        iter_dir = None
        try:
            if session_dir is not None:
                iter_dir = session_dir / f"iter_{idx+1}"
                iter_dir.mkdir(parents=True, exist_ok=True)
        except Exception:
            iter_dir = None
        modal = _analyze_for_modal(analyzer, screenshot_path, debug_dir=iter_dir)
        try:
            analyzer.openai_logger.info(f"[Modal] OpenAI result: {json.dumps(modal)}")
            analyzer.openai_logger.info(
                f"[Modal] Type='{modal.get('type')}', button_text='{modal.get('button_text')}', alt_texts={modal.get('alt_texts') or []}"
            )
        except Exception:
            pass
        # If the model summary explicitly indicates no modal, respect that
        if _summary_indicates_no_modal(str(modal.get("summary") or "")):
            try:
                analyzer.openai_logger.info("[Modal] Summary indicates no modal; exiting modal loop")
            except Exception:
                pass
            cleared = True
            break
        if not modal.get("has_modal") or modal.get("type") == "none":
            cleared = True
            try:
                analyzer.openai_logger.info("[Modal] No modal detected; exiting modal loop")
            except Exception:
                pass
            break
        modal_type = str(modal.get("type") or "unknown").lower()
        # Strict behavior: click exactly the button_text returned by the model using Rekognition
        btn_label = str(modal.get("button_text") or "").strip()
        if btn_label:
            try:
                analyzer.openai_logger.info(f"[Modal] Clicking exact model label via Rekognition: '{btn_label}'")
            except Exception:
                pass
            clicked = _click_by_rekognition_tiler(analyzer, screenshot_path, btn_label, debug_parent=iter_dir)
            if clicked:
                actions.append({"modal": modal_type, "clicked": btn_label})
                # proceed to next iteration to verify modal cleared
                continue
            # Optional: for explicit cancel type, press ESC if Rekognition couldn't find the label
            if modal_type == "cancel":
                try:
                    analyzer.openai_logger.info("[Modal] Rekognition did not find model label; pressing ESC as cancel fallback")
                except Exception:
                    pass
                pyautogui.press('esc')
                actions.append({"modal": modal_type, "clicked": "esc"})
                continue
            # If not cancel and click failed, stop modal loop to avoid unintended actions
            cleared = False
            try:
                analyzer.openai_logger.warning("[Modal] Model label not found by Rekognition; stopping modal loop")
            except Exception:
                pass
            break
        else:
            # No button_text provided; cannot act safely
            try:
                analyzer.openai_logger.warning("[Modal] Model did not provide button_text; stopping modal loop")
            except Exception:
                pass
            cleared = False
            break
    # Save session summary
    try:
        if session_dir is not None:
            (session_dir / 'actions.json').write_text(json.dumps({"actions": actions, "cleared": cleared}, ensure_ascii=False, indent=2), encoding='utf-8')
    except Exception:
        pass
    return {"success": True, "actions": actions, "cleared": cleared, "debug_dir": str(session_dir) if session_dir is not None else None}



def click_save_dialog_once(analyzer: OpenAIAnalyzer) -> Dict:
    """One-shot check: if a Save/Confirm-type dialog is visible, click the Save button once.

    - No loops, no retries, no tab handling. Returns a small result dict.
    {
      "checked": true,
      "clicked": bool,
      "label": str|None,
      "reason": str
    }
    """
    try:
        # Capture current screen
        screenshot_path = analyzer.capture_full_resolution_screenshot()
        if not screenshot_path:
            return {"checked": True, "clicked": False, "label": None, "reason": "no_screenshot"}

        # Ask OpenAI once whether a modal is present and which button to click
        modal = _analyze_for_modal(analyzer, screenshot_path, debug_dir=None)
        if not isinstance(modal, dict):
            return {"checked": True, "clicked": False, "label": None, "reason": "no_result"}

        # Only act when it is clearly a save/confirm-type modal
        mtype = str(modal.get("type") or "").strip().lower()
        if not modal.get("has_modal") or mtype != "save":
            return {"checked": True, "clicked": False, "label": None, "reason": "no_save_modal"}

        # Prefer exact label suggested by the model; fallback to alternatives; then common save labels
        candidates: List[str] = []
        btn = str(modal.get("button_text") or "").strip()
        if btn:
            candidates.append(btn)
        try:
            for alt in modal.get("alt_texts") or []:
                alt = str(alt or "").strip()
                if alt:
                    candidates.append(alt)
        except Exception:
            pass
        # Add common save keywords as a last resort
        for k in SAVE_KEYWORDS:
            if k not in candidates:
                candidates.append(k)

        # Try each candidate once; handle 'Download' with stricter helper, else strict Rekognition
        for label in candidates:
            lbl = str(label or "").strip()
            if not lbl:
                continue
            # Special-case: 'Download' should use strict helper to avoid top-left misclicks
            if lbl.lower() == 'download':
                try:
                    from .xero_util.attach_files_dialog_utils import click_download_link_if_present as _click_download
                except Exception:
                    _click_download = None  # type: ignore
                if _click_download is not None and getattr(analyzer, 'app', None) is not None:
                    try:
                        if _click_download(analyzer.app):  # type: ignore[arg-type]
                            return {"checked": True, "clicked": True, "label": lbl, "reason": "clicked"}
                    except Exception:
                        pass
                # Fallback to Rekognition strict include if helper unavailable
                if _click_by_rekognition_tiler(analyzer, screenshot_path, lbl, debug_parent=None, allow_fallback=False):
                    return {"checked": True, "clicked": True, "label": lbl, "reason": "clicked"}
                continue
            # Non-download labels: require strict include (no fallback) to reduce false positives
            if _click_by_rekognition_tiler(analyzer, screenshot_path, lbl, debug_parent=None, allow_fallback=False):
                return {"checked": True, "clicked": True, "label": lbl, "reason": "clicked"}

        return {"checked": True, "clicked": False, "label": None, "reason": "not_found"}
    except Exception as e:
        try:
            if hasattr(analyzer, 'openai_logger'):
                analyzer.openai_logger.error(f"[ModalOnce] Error: {e}")
        except Exception:
            pass
        return {"checked": True, "clicked": False, "label": None, "reason": "exception"}


def click_close_dialog_once(analyzer: OpenAIAnalyzer) -> Dict:
    """One-shot check: if a Cancel/Close-type dialog is visible, click the dismiss button once.

    - No loops or retries. If no cancel/close modal is present, do nothing.
    Returns dict like click_save_dialog_once.
    """
    try:
        screenshot_path = analyzer.capture_full_resolution_screenshot()
        if not screenshot_path:
            return {"checked": True, "clicked": False, "label": None, "reason": "no_screenshot"}

        modal = _analyze_for_modal(analyzer, screenshot_path, debug_dir=None)
        if not isinstance(modal, dict):
            return {"checked": True, "clicked": False, "label": None, "reason": "no_result"}

        mtype = str(modal.get("type") or "").strip().lower()
        if not modal.get("has_modal") or mtype != "cancel":
            return {"checked": True, "clicked": False, "label": None, "reason": "no_cancel_modal"}

        candidates: List[str] = []
        btn = str(modal.get("button_text") or "").strip()
        if btn:
            candidates.append(btn)
        try:
            for alt in modal.get("alt_texts") or []:
                alt = str(alt or "").strip()
                if alt:
                    candidates.append(alt)
        except Exception:
            pass
        # Add common cancel/close keywords at the end
        for k in CANCEL_KEYWORDS:
            if k not in candidates:
                candidates.append(k)

        for label in candidates:
            if _click_by_rekognition_tiler(analyzer, screenshot_path, label, debug_parent=None, allow_fallback=True):
                return {"checked": True, "clicked": True, "label": label, "reason": "clicked"}

        return {"checked": True, "clicked": False, "label": None, "reason": "not_found"}
    except Exception as e:
        try:
            if hasattr(analyzer, 'openai_logger'):
                analyzer.openai_logger.error(f"[ModalOnceClose] Error: {e}")
        except Exception:
            pass
        return {"checked": True, "clicked": False, "label": None, "reason": "exception"}

