"""Boris engine - execution and monitoring (merged from executor + monitor)."""
import enum
import logging
import os
import re
import shutil
import subprocess
import sys
import tempfile
from dataclasses import dataclass
from datetime import datetime
from typing import Optional

# Force unbuffered stdout for real-time output on Windows
os.environ.setdefault("PYTHONUNBUFFERED", "1")

from state import Milestone, UIMilestone

IS_WINDOWS = sys.platform == "win32"
logger = logging.getLogger("boris.engine")

# Regex to strip ANSI escape codes
_ANSI_RE = re.compile(r"\x1b\[[0-9;]*[a-zA-Z]")

# Directories (relative to Boris install dir)
_BORIS_DIR = os.path.dirname(os.path.abspath(__file__))
_LOGS_DIR = os.path.join(_BORIS_DIR, "logs")
_BORIS_PROMPT_PATH = os.path.join(_BORIS_DIR, "boris_prompt.md")

# Resolve commands to full paths so shell=False works on Windows (.cmd files)
CLAUDE_CMD = shutil.which("claude") or "claude"

# Cached Boris system prompt
_boris_prompt_cache = None


def _load_boris_prompt() -> str:
    """Load Boris's management prompt from boris_prompt.md. Cached after first load."""
    global _boris_prompt_cache
    if _boris_prompt_cache is not None:
        return _boris_prompt_cache
    try:
        with open(_BORIS_PROMPT_PATH, "r", encoding="utf-8") as f:
            _boris_prompt_cache = f.read().strip()
        logger.debug("Loaded Boris prompt: %d chars", len(_boris_prompt_cache))
    except FileNotFoundError:
        logger.warning("boris_prompt.md not found at %s", _BORIS_PROMPT_PATH)
        _boris_prompt_cache = ""
    return _boris_prompt_cache
DAVELOOP_CMD = shutil.which("daveloop") or "daveloop"

# Limits
DEFAULT_MAX_ITERATIONS = 15


def _clean_output(text: str) -> str:
    """Strip ANSI escape codes and non-ASCII chars for clean analysis."""
    text = _ANSI_RE.sub("", text)
    # Replace non-ASCII (emoji, box-drawing) with spaces
    text = text.encode("ascii", errors="replace").decode("ascii")
    return text


# --- Execution (from executor.py) ---


@dataclass
class ExecutionResult:
    output: str
    exit_code: int
    resolved: bool
    log_path: Optional[str] = None


def _boris_commentary(reasoning: dict, step_num: int, accomplishments: list):
    """Boris reports what DaveLoop accomplished so far and what he's cooking next."""
    known = reasoning.get("KNOWN", "").strip()
    hypothesis = reasoning.get("HYPOTHESIS", "").strip()
    next_action = reasoning.get("NEXT", "").strip()

    print(flush=True)
    print(f"  [Boris] === DaveLoop Check-in #{step_num} ===", flush=True)

    # What DaveLoop accomplished since last reasoning block
    if accomplishments:
        print(f"  [Boris] Done so far:", flush=True)
        for a in accomplishments:
            print(f"  [Boris]   - {a}", flush=True)
    elif step_num == 1:
        print(f"  [Boris] Starting up, gathering info.", flush=True)

    # What DaveLoop knows now
    if known:
        print(f"  [Boris] Knows: {known}", flush=True)

    # What DaveLoop is about to do
    if hypothesis:
        print(f"  [Boris] Thinking: {hypothesis}", flush=True)
    if next_action:
        print(f"  [Boris] Next: {next_action}", flush=True)

    print(f"  [Boris] ===========================", flush=True)
    print(flush=True)


def _parse_accomplishment(clean_line: str) -> Optional[str]:
    """Parse a DaveLoop output line into a human-readable accomplishment, or None."""
    # File writes
    write_match = re.search(r"Write\((.+?)\)", clean_line)
    if write_match:
        return f"Created {write_match.group(1).strip()}"

    # File edits
    edit_match = re.search(r"Edit\((.+?)\)", clean_line)
    if edit_match:
        return f"Edited {edit_match.group(1).strip()}"

    # Bash commands
    bash_match = re.search(r"Bash\((.+?)\)", clean_line)
    if bash_match:
        cmd = bash_match.group(1).strip()
        if "test" in cmd.lower() or "pytest" in cmd.lower():
            return f"Ran tests: {cmd}"
        elif "mkdir" in cmd.lower():
            return f"Created directory: {cmd}"
        elif "pip install" in cmd.lower() or "npm install" in cmd.lower():
            return f"Installed dependencies: {cmd}"
        else:
            return f"Ran: {cmd}"

    # Tool results - success/failure
    if "? ?" in clean_line or "PASS" in clean_line.upper():
        return "Verification passed"
    if "? ?" in clean_line or "FAIL" in clean_line.upper():
        if "test" in clean_line.lower():
            return "Test failed - will retry"

    # DaveLoop iteration markers
    if "ITERATION" in clean_line.upper() and re.search(r"\d+", clean_line):
        iter_match = re.search(r"(\d+)", clean_line)
        if iter_match:
            return f"DaveLoop iteration {iter_match.group(1)}"

    # RESOLVED signal
    if "[DAVELOOP:RESOLVED]" in clean_line:
        return "DaveLoop reports: RESOLVED"

    return None


def _check_off_rail(clean_line: str, milestone: Milestone) -> Optional[str]:
    """Detect if DaveLoop is going off-rail. Returns interrupt message or None."""
    lower = clean_line.lower()

    # Detect building wrong files
    allowed_files = set((milestone.files_to_create or []) + (milestone.files_to_modify or []))
    if allowed_files:
        # Check for Write tool creating files outside scope
        write_match = re.search(r"Write\((.+?)\)", clean_line)
        if write_match:
            written_file = write_match.group(1).strip()
            # Normalize: strip path prefixes, compare basenames
            written_base = os.path.basename(written_file)
            allowed_bases = {os.path.basename(f) for f in allowed_files}
            if written_base and written_base not in allowed_bases:
                return (
                    f"wait - you are creating {written_file} which is outside the scope of "
                    f"{milestone.id}. Only touch: {', '.join(allowed_files)}. "
                    f"Focus on {milestone.id}: {milestone.title} only."
                )

    # Detect building entire project / other milestones
    off_rail_phrases = [
        "build the entire project",
        "build everything",
        "implement all milestones",
        "implement all features",
        "building the full",
    ]
    for phrase in off_rail_phrases:
        if phrase in lower:
            return (
                f"wait - you are going off-rail. You are only building {milestone.id}: "
                f"{milestone.title}. Do NOT build the entire project. "
                f"Focus ONLY on {milestone.id}."
            )

    # Detect mentioning other milestone IDs in action context
    other_ms = re.findall(r"\bM\d+\b", clean_line)
    for m_id in other_ms:
        if m_id != milestone.id and ("building" in lower or "implement" in lower or "creating" in lower):
            return (
                f"wait - you mentioned {m_id} but you should only be working on "
                f"{milestone.id}: {milestone.title}. Stay in scope."
            )

    return None


def _send_interrupt(process, message: str, boris_log):
    """Send a text interrupt to DaveLoop's stdin."""
    try:
        if process.stdin and not process.stdin.closed:
            process.stdin.write(f"{message}\n".encode("utf-8"))
            process.stdin.flush()
            log_msg = f"[Boris INTERRUPT] {message}"
            print(f"\n  {log_msg}\n", flush=True)
            logger.warning(log_msg)
            if boris_log:
                boris_log.write(f"\n{log_msg}\n")
    except (OSError, BrokenPipeError):
        logger.debug("Could not send interrupt to DaveLoop stdin")


def run(prompt: str, project_dir: str, max_iterations: int = None,
        milestone: Milestone = None) -> ExecutionResult:
    """Spawn DaveLoop with the crafted prompt. Boris manages, DaveLoop builds.

    If milestone is provided, Boris monitors DaveLoop's reasoning blocks in real-time
    and intervenes via text interrupt if DaveLoop goes off-rail.
    """
    max_iter = max_iterations if max_iterations is not None else DEFAULT_MAX_ITERATIONS
    temp_file = None
    boris_log = None

    try:
        # Write prompt to temp file for DaveLoop
        temp_file = tempfile.NamedTemporaryFile(
            mode="w", suffix=".txt", delete=False, encoding="utf-8"
        )
        temp_file.write(prompt)
        temp_file.close()

        # Boris spawns DaveLoop as the worker
        cmd = [
            DAVELOOP_CMD,
            "-f", temp_file.name,
            "-d", project_dir,
            "-m", str(max_iter),
            "-v",
        ]

        env = os.environ.copy()
        env["PYTHONIOENCODING"] = "utf-8"

        print(f"  [Boris] Spawning DaveLoop: max_iter={max_iter}, project={project_dir}", flush=True)
        logger.info("Spawning DaveLoop: max_iter=%d, project=%s", max_iter, project_dir)
        logger.debug("Prompt length: %d chars", len(prompt))

        # Boris's own log for this execution
        log_path = _setup_log(project_dir)
        boris_log = open(log_path, "w", encoding="utf-8")
        boris_log.write(f"=== Boris Execution Log ===\n")
        boris_log.write(f"Timestamp: {datetime.now().isoformat()}\n")
        boris_log.write(f"Project: {project_dir}\n")
        boris_log.write(f"Max iterations: {max_iter}\n")
        boris_log.write(f"Prompt file: {temp_file.name}\n")
        boris_log.write(f"Prompt length: {len(prompt)} chars\n")
        boris_log.write(f"===========================\n\n")

        # Run DaveLoop with real-time streaming + stdin for interrupts
        # bufsize=0 disables OS-level pipe buffering for real-time output on Windows
        process = subprocess.Popen(
            cmd,
            stdin=subprocess.PIPE,
            stdout=subprocess.PIPE,
            stderr=subprocess.STDOUT,
            cwd=project_dir,
            env=env,
            bufsize=0,
        )

        output_lines = []
        # Reasoning block parser state
        in_reasoning = False
        reasoning_lines = []
        reasoning_count = 0
        accomplishments = []       # tracks what DaveLoop did since last reasoning block
        all_accomplishments = []   # cumulative for the whole run
        interrupt_count = 0
        MAX_INTERRUPTS = 3  # After 3 interrupts, let DaveLoop finish and fail at verdict

        for raw_line in process.stdout:
            line = raw_line.decode("utf-8", errors="replace")
            sys.stdout.write(line)
            sys.stdout.flush()
            output_lines.append(line)
            boris_log.write(line)

            # Parse cleaned line for monitoring
            clean = _clean_output(line).strip()

            # --- Track accomplishments between reasoning blocks ---
            if not in_reasoning:
                acc = _parse_accomplishment(clean)
                if acc:
                    accomplishments.append(acc)
                    all_accomplishments.append(acc)

            # --- Reasoning block detection ---
            if "REASONING" in clean and ("===" in clean or "---" in clean or "KNOWN" in clean):
                in_reasoning = True
                reasoning_lines = []
                continue

            if in_reasoning:
                # Check for end of reasoning block
                if clean.startswith("===") or clean.startswith("---") or clean.startswith("└"):
                    in_reasoning = False
                    reasoning_count += 1
                    # Parse collected reasoning
                    reasoning = {}
                    for rl in reasoning_lines:
                        for key in ("KNOWN", "UNKNOWN", "HYPOTHESIS", "NEXT", "WHY"):
                            if rl.startswith(key + ":") or rl.startswith(key + " :"):
                                reasoning[key] = rl.split(":", 1)[1].strip()
                    if reasoning:
                        _boris_commentary(reasoning, reasoning_count, accomplishments)
                    # Reset per-block accomplishments, keep cumulative
                    accomplishments = []
                    reasoning_lines = []
                else:
                    reasoning_lines.append(clean)

            # --- Off-rail detection ---
            if milestone and interrupt_count < MAX_INTERRUPTS:
                interrupt_msg = _check_off_rail(clean, milestone)
                if interrupt_msg:
                    _send_interrupt(process, interrupt_msg, boris_log)
                    interrupt_count += 1
                    if interrupt_count >= MAX_INTERRUPTS:
                        warn = (
                            f"[Boris] Sent {MAX_INTERRUPTS} interrupts. "
                            f"DaveLoop keeps going off-rail. Will check at verdict."
                        )
                        print(f"\n  {warn}\n", flush=True)
                        logger.warning(warn)
                        if boris_log:
                            boris_log.write(f"\n{warn}\n")

        process.wait()
        output = "".join(output_lines)

        # Boris end-of-run summary
        if all_accomplishments:
            print(flush=True)
            print(f"  [Boris] === DaveLoop Run Complete ===", flush=True)
            print(f"  [Boris] Total actions tracked: {len(all_accomplishments)}", flush=True)
            for a in all_accomplishments:
                print(f"  [Boris]   - {a}", flush=True)
            print(f"  [Boris] =============================", flush=True)
            print(flush=True)

        boris_log.write(f"\n=== DaveLoop exit code: {process.returncode} ===\n")
        if interrupt_count > 0:
            boris_log.write(f"Boris interrupts sent: {interrupt_count}\n")
        if all_accomplishments:
            boris_log.write(f"Tracked accomplishments: {len(all_accomplishments)}\n")
            for a in all_accomplishments:
                boris_log.write(f"  - {a}\n")

        # Check if DaveLoop resolved the milestone
        resolved = "[DAVELOOP:RESOLVED]" in output

        # Also find DaveLoop's own log path
        daveloop_log = None
        log_match = re.search(r"Logs:\s*(\S+)", output)
        if log_match:
            daveloop_log = log_match.group(1)
            boris_log.write(f"DaveLoop log: {daveloop_log}\n")

        logger.info(
            "DaveLoop finished. Exit code: %d, Resolved: %s, Interrupts: %d",
            process.returncode, resolved, interrupt_count
        )

        return ExecutionResult(
            output=output,
            exit_code=process.returncode,
            resolved=resolved,
            log_path=log_path,
        )

    except FileNotFoundError:
        return ExecutionResult(
            output=f"Error: '{DAVELOOP_CMD}' not found. Install with: pip install daveloop",
            exit_code=127,
            resolved=False,
        )
    except subprocess.SubprocessError as e:
        return ExecutionResult(
            output=f"Subprocess error: {e}",
            exit_code=1,
            resolved=False,
        )
    finally:
        if temp_file is not None:
            try:
                os.unlink(temp_file.name)
            except OSError:
                pass
        if boris_log is not None:
            try:
                boris_log.close()
            except OSError:
                pass


def _setup_log(project_dir: str) -> str:
    """Create a log file path for this execution."""
    os.makedirs(_LOGS_DIR, exist_ok=True)
    timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
    return os.path.join(_LOGS_DIR, f"boris_exec_{timestamp}.log")


# --- Monitoring (from monitor.py) ---


class Verdict(enum.Enum):
    RESOLVED = "RESOLVED"
    OFF_PLAN = "OFF_PLAN"
    FAILED = "FAILED"


@dataclass
class VerdictResult:
    verdict: Verdict
    reason: str


def check(result: ExecutionResult, milestone: Milestone) -> VerdictResult:
    """Check execution result and return a verdict."""
    print(f"  [Boris] Evaluating DaveLoop result for {milestone.id}...", flush=True)

    if result.resolved:
        print(f"  [Boris] DaveLoop self-reported RESOLVED for {milestone.id}", flush=True)
        return VerdictResult(verdict=Verdict.RESOLVED, reason="DaveLoop reported resolution")

    if result.exit_code != 0:
        print(f"  [Boris] DaveLoop exited with code {result.exit_code} for {milestone.id}", flush=True)
        # Get tail of output for reason
        lines = result.output.strip().splitlines()
        tail = "\n".join(lines[-20:]) if len(lines) > 20 else result.output.strip()
        return VerdictResult(
            verdict=Verdict.FAILED,
            reason=f"Process exited with code {result.exit_code}. Output tail:\n{tail}",
        )

    # Use Claude for nuanced analysis
    print(f"  [Boris] Running Claude verdict analysis for {milestone.id}...", flush=True)
    return _analyze_with_claude(result.output, milestone)


def _analyze_with_claude(output: str, milestone: Milestone) -> VerdictResult:
    """Use Claude CLI to analyze whether output achieved the milestone."""
    criteria_text = "\n".join(f"- {c}" for c in milestone.acceptance_criteria)
    # Clean and truncate output
    clean = _clean_output(output)
    output_lines = clean.strip().splitlines()
    truncated = "\n".join(output_lines[-300:]) if len(output_lines) > 300 else clean.strip()

    boris_brain = _load_boris_prompt()
    prompt = ""
    if boris_brain:
        prompt += boris_brain + "\n\n---\n\n"
    prompt += (
        f"Analyze this build output. Did it achieve these acceptance criteria:\n"
        f"{criteria_text}\n\n"
        f"Build output:\n{truncated}\n\n"
        f"Answer with exactly one of: RESOLVED, OFF_PLAN, FAILED. "
        f"Then explain why on the next line."
    )

    try:
        env = os.environ.copy()
        env["PYTHONIOENCODING"] = "utf-8"
        result = subprocess.run(
            [CLAUDE_CMD, "-p", "--no-session-persistence"],
            input=prompt,
            capture_output=True,
            timeout=120,
            env=env,
            encoding="utf-8",
            errors="replace",
        )
        if result.returncode != 0:
            return VerdictResult(
                verdict=Verdict.FAILED,
                reason="Could not analyze output",
            )

        response = result.stdout.strip()
        lines = response.splitlines()
        if not lines:
            return VerdictResult(verdict=Verdict.FAILED, reason="Empty analysis response")

        first_line = lines[0].strip().upper()
        reason = "\n".join(lines[1:]).strip() if len(lines) > 1 else "No explanation provided"

        if "RESOLVED" in first_line:
            return VerdictResult(verdict=Verdict.RESOLVED, reason=reason)
        elif "OFF_PLAN" in first_line:
            return VerdictResult(verdict=Verdict.OFF_PLAN, reason=reason)
        else:
            return VerdictResult(verdict=Verdict.FAILED, reason=reason)

    except (subprocess.SubprocessError, subprocess.TimeoutExpired):
        return VerdictResult(
            verdict=Verdict.FAILED,
            reason="Could not analyze output",
        )


# --- UI Testing (DaveLoop v1.4) ---


# Off-rail phrases specific to UI tester mode (building new stuff instead of testing)
_UI_OFF_RAIL_PHRASES = [
    "add new feature",
    "implement new",
    "create new endpoint",
    "add new screen",
    "build new component",
]


def _parse_ui_accomplishment(clean_line: str, ui_milestone: UIMilestone) -> Optional[str]:
    """Parse a DaveLoop output line into a UI-specific accomplishment, or None.

    Falls back to the standard _parse_accomplishment for non-UI-specific lines.
    """
    clean = clean_line.strip()
    lower = clean.lower()

    # Screenshots
    if "screenshot" in lower and ("saved" in lower or "captured" in lower or "taken" in lower):
        return "Captured screenshot"

    # Playwright/Maestro test runs
    if "maestro test" in lower or "playwright test" in lower:
        return f"Ran UI test: {clean}"

    # Issue tracking
    issue_match = re.search(r"ISSUE FOUND:\s*(.+)", clean)
    if issue_match:
        ui_milestone.issues_found.append(issue_match.group(1))
        return f"Found issue: {issue_match.group(1)}"

    fix_match = re.search(r"FIX APPLIED:\s*(.+)", clean)
    if fix_match:
        ui_milestone.issues_fixed.append(fix_match.group(1))
        return f"Fixed: {fix_match.group(1)}"

    # Fall back to standard parsing
    return _parse_accomplishment(clean)


def _check_ui_off_rail(clean_line: str, ui_milestone: UIMilestone) -> Optional[str]:
    """Detect if DaveLoop is going off-rail in UI tester mode (building instead of testing)."""
    lower = clean_line.lower()

    for phrase in _UI_OFF_RAIL_PHRASES:
        if phrase in lower:
            return (
                f"wait - you are in UI TESTER MODE. You should be testing "
                f"{ui_milestone.id}: {ui_milestone.title}, not building new features. "
                f"Test existing UI and fix visual/UX issues only."
            )

    return None


def run_ui_test(prompt: str, project_dir: str, max_iterations: int = None,
                ui_milestone: UIMilestone = None) -> ExecutionResult:
    """Spawn DaveLoop for UI testing. Thin wrapper around run() with UI-specific monitoring.

    Boris ships DaveLoop the prompt and monitors for UI-specific accomplishments
    and off-rail behavior. DaveLoop handles the actual testing.
    """
    # Use the same run() infrastructure - DaveLoop does the work
    # We just swap in UI-specific off-rail detection via a temporary Milestone adapter
    # so the existing run() monitoring pipeline picks it up.

    # Create a lightweight Milestone-compatible object for run()'s off-rail checks.
    # run() expects a Milestone with files_to_create/files_to_modify for scope checks.
    # For UI testing, we don't restrict files - DaveLoop can touch test files freely.
    adapter = Milestone(
        id=ui_milestone.id if ui_milestone else "UI",
        title=ui_milestone.title if ui_milestone else "UI Test",
        description=ui_milestone.description if ui_milestone else "",
        depends_on=[],
        acceptance_criteria=ui_milestone.acceptance_criteria if ui_milestone else [],
        files_to_create=[],  # No file restrictions in UI test mode
        files_to_modify=[],
    )

    result = run(prompt, project_dir, max_iterations, milestone=adapter)

    # Post-process: scan output for UI-specific markers
    if ui_milestone:
        for line in result.output.splitlines():
            clean = _clean_output(line).strip()
            issue_match = re.search(r"ISSUE FOUND:\s*(.+)", clean)
            if issue_match and issue_match.group(1) not in ui_milestone.issues_found:
                ui_milestone.issues_found.append(issue_match.group(1))
            fix_match = re.search(r"FIX APPLIED:\s*(.+)", clean)
            if fix_match and fix_match.group(1) not in ui_milestone.issues_fixed:
                ui_milestone.issues_fixed.append(fix_match.group(1))

    return result


def check_ui(result: ExecutionResult, ui_milestone: UIMilestone, test_tool: str) -> VerdictResult:
    """Check UI test execution result and return a verdict."""
    print(f"  [Boris] Evaluating UI test result for {ui_milestone.id}...", flush=True)

    if result.resolved:
        print(f"  [Boris] DaveLoop self-reported RESOLVED for {ui_milestone.id}", flush=True)
        return VerdictResult(verdict=Verdict.RESOLVED, reason="DaveLoop reported resolution")

    if result.exit_code != 0:
        print(f"  [Boris] DaveLoop exited with code {result.exit_code} for {ui_milestone.id}", flush=True)
        lines = result.output.strip().splitlines()
        tail = "\n".join(lines[-20:]) if len(lines) > 20 else result.output.strip()
        return VerdictResult(
            verdict=Verdict.FAILED,
            reason=f"Process exited with code {result.exit_code}. Output tail:\n{tail}",
        )

    # Use Claude for UI-specific verdict
    print(f"  [Boris] Running Claude UI verdict analysis for {ui_milestone.id}...", flush=True)
    return _analyze_ui_verdict(result.output, ui_milestone, test_tool)


def _analyze_ui_verdict(output: str, ui_milestone: UIMilestone, test_tool: str) -> VerdictResult:
    """Use Claude CLI to analyze whether UI testing output achieved the milestone."""
    criteria_text = "\n".join(f"- {c}" for c in ui_milestone.acceptance_criteria)
    clean = _clean_output(output)
    output_lines = clean.strip().splitlines()
    truncated = "\n".join(output_lines[-300:]) if len(output_lines) > 300 else clean.strip()

    prompt = (
        f"Analyze this UI testing output. Did DaveLoop:\n"
        f"1. Run the specified tests using {test_tool}?\n"
        f"2. Find and report UI issues?\n"
        f"3. Fix the issues it found?\n"
        f"4. Achieve these acceptance criteria:\n{criteria_text}\n\n"
        f"Test output:\n{truncated}\n\n"
        f"Answer with exactly one of: RESOLVED, OFF_PLAN, FAILED.\n"
        f"Then explain why on the next line."
    )

    try:
        env = os.environ.copy()
        env["PYTHONIOENCODING"] = "utf-8"
        result = subprocess.run(
            [CLAUDE_CMD, "-p", "--no-session-persistence"],
            input=prompt,
            capture_output=True,
            timeout=120,
            env=env,
            encoding="utf-8",
            errors="replace",
        )
        if result.returncode != 0:
            return VerdictResult(verdict=Verdict.FAILED, reason="Could not analyze UI output")

        response = result.stdout.strip()
        lines = response.splitlines()
        if not lines:
            return VerdictResult(verdict=Verdict.FAILED, reason="Empty analysis response")

        first_line = lines[0].strip().upper()
        reason = "\n".join(lines[1:]).strip() if len(lines) > 1 else "No explanation provided"

        if "RESOLVED" in first_line:
            return VerdictResult(verdict=Verdict.RESOLVED, reason=reason)
        elif "OFF_PLAN" in first_line:
            return VerdictResult(verdict=Verdict.OFF_PLAN, reason=reason)
        else:
            return VerdictResult(verdict=Verdict.FAILED, reason=reason)

    except (subprocess.SubprocessError, subprocess.TimeoutExpired):
        return VerdictResult(verdict=Verdict.FAILED, reason="Could not analyze UI output")
