"""
Test Case API Routes
"""

import yaml
from fastapi import APIRouter, HTTPException
from pydantic import BaseModel
from typing import List, Optional, Dict, Any

router = APIRouter()


class CaseCreate(BaseModel):
    """Request model for creating a test case."""
    name: str
    description: Optional[str] = None
    execution_mode: str = "screen"  # 'screen', 'headless', 'mixed'
    dsl_content: Optional[str] = None


class DSLValidationRequest(BaseModel):
    """Request model for DSL validation."""
    dsl_content: str


class DSLValidationError(BaseModel):
    """Single validation error with line number."""
    message: str
    line: Optional[int] = None
    column: Optional[int] = None
    severity: str = "error"  # "error" | "warning"


class DSLValidationResponse(BaseModel):
    """Response model for DSL validation."""
    valid: bool
    errors: List[str] = []  # Keep for backward compatibility
    error_details: List[DSLValidationError] = []  # New detailed errors
    warnings: List[str] = []  # Keep for backward compatibility
    warning_details: List[DSLValidationError] = []  # New detailed warnings
    step_count: int = 0


def find_line_for_step(dsl_content: str, step_index: int) -> int:
    """Find the line number for a specific step in the YAML content."""
    lines = dsl_content.split('\n')
    step_counter = -1  # Start at -1 because we look for '- ' which indicates a new step

    for line_num, line in enumerate(lines, 1):
        # Check if this line starts a new step (starts with '- ' within steps section)
        if line.strip().startswith('- '):
            step_counter += 1
            if step_counter == step_index:
                return line_num
    return 1  # Default to line 1 if not found


def find_line_for_key(dsl_content: str, key: str) -> int:
    """Find the line number for a specific key in the YAML content."""
    lines = dsl_content.split('\n')
    for line_num, line in enumerate(lines, 1):
        if line.strip().startswith(f'{key}:'):
            return line_num
    return 1


def parse_yaml_error_location(error_msg: str) -> tuple[Optional[int], Optional[int]]:
    """Extract line and column from YAML error message."""
    import re
    # Common patterns: "line 5, column 3" or "line 5"
    match = re.search(r'line\s+(\d+)(?:,\s*column\s+(\d+))?', error_msg)
    if match:
        line = int(match.group(1))
        column = int(match.group(2)) if match.group(2) else None
        return line, column
    return None, None


def validate_dsl(dsl_content: str) -> tuple[bool, List[str], List[str], int, List[DSLValidationError], List[DSLValidationError]]:
    """Validate DSL content and return (is_valid, errors, warnings, step_count, error_details, warning_details)."""
    errors: List[str] = []
    warnings: List[str] = []
    error_details: List[DSLValidationError] = []
    warning_details: List[DSLValidationError] = []
    step_count = 0

    if not dsl_content or not dsl_content.strip():
        err = DSLValidationError(message="DSL content is empty. Please add test steps.", line=1, severity="error")
        errors.append(err.message)
        error_details.append(err)
        return False, errors, warnings, 0, error_details, warning_details

    # Try to parse YAML
    try:
        dsl_data = yaml.safe_load(dsl_content)
    except yaml.YAMLError as e:
        error_msg = str(e)
        line, column = parse_yaml_error_location(error_msg)

        # Make error message more user-friendly
        if "mapping values are not allowed here" in error_msg:
            msg = f"YAML syntax error: Incorrect indentation. Check that all nested items are properly indented."
        elif "expected ',' or ']', but got '" in error_msg or "could not determine a constructor" in error_msg:
            msg = f"YAML syntax error: Invalid character or missing quote."
        else:
            msg = f"YAML parse error: {error_msg}"

        err = DSLValidationError(message=msg, line=line or 1, column=column, severity="error")
        errors.append(err.message)
        error_details.append(err)
        return False, errors, warnings, 0, error_details, warning_details
    except Exception as e:
        err = DSLValidationError(message=f"Failed to parse DSL: {str(e)}", line=1, severity="error")
        errors.append(err.message)
        error_details.append(err)
        return False, errors, warnings, 0, error_details, warning_details

    if dsl_data is None:
        err = DSLValidationError(message="DSL content is empty or contains only comments.", line=1, severity="error")
        errors.append(err.message)
        error_details.append(err)
        return False, errors, warnings, 0, error_details, warning_details

    if not isinstance(dsl_data, dict):
        err = DSLValidationError(message="DSL root must be an object (e.g., 'steps:').", line=1, severity="error")
        errors.append(err.message)
        error_details.append(err)
        return False, errors, warnings, 0, error_details, warning_details

    # Check for steps
    steps = dsl_data.get("steps", [])
    if not steps:
        line = find_line_for_key(dsl_content, "steps") if "steps" in dsl_data else 1
        err = DSLValidationError(message="No 'steps' found in DSL. At least one step is required.", line=line, severity="error")
        errors.append(err.message)
        error_details.append(err)
        return False, errors, warnings, 0, error_details, warning_details

    if not isinstance(steps, list):
        line = find_line_for_key(dsl_content, "steps")
        err = DSLValidationError(message="'steps' must be a list (array).", line=line, severity="error")
        errors.append(err.message)
        error_details.append(err)
        return False, errors, warnings, 0, error_details, warning_details

    step_count = len(steps)

    # Validate each step
    valid_actions = {
        "launch", "wait", "screenshot", "assert", "click", "double_click",
        "right_click", "type", "hotkey", "press", "drag", "scroll", "move",
        "send_keys", "wait_output", "exec", "if", "loop", "for_each", "call"
    }

    for i, step in enumerate(steps):
        step_line = find_line_for_step(dsl_content, i)

        if not isinstance(step, dict):
            err = DSLValidationError(
                message=f"Step {i+1}: Must be an object, not {type(step).__name__}.",
                line=step_line,
                severity="error"
            )
            errors.append(err.message)
            error_details.append(err)
            continue

        # Check action
        action = step.get("action")
        if not action:
            err = DSLValidationError(
                message=f"Step {i+1}: Missing 'action' field.",
                line=step_line,
                severity="error"
            )
            errors.append(err.message)
            error_details.append(err)
        elif action not in valid_actions:
            err = DSLValidationError(
                message=f"Step {i+1}: Unknown action '{action}'. Valid actions: {', '.join(sorted(valid_actions))}",
                line=step_line,
                severity="error"
            )
            errors.append(err.message)
            error_details.append(err)

        # Check for name (warning if missing)
        if not step.get("name"):
            warn = DSLValidationError(
                message=f"Step {i+1}: Consider adding a 'name' for better readability.",
                line=step_line,
                severity="warning"
            )
            warnings.append(warn.message)
            warning_details.append(warn)

        # Action-specific validation
        if action == "exec" and not step.get("command"):
            warn = DSLValidationError(
                message=f"Step {i+1}: 'exec' action should have a 'command' field.",
                line=step_line,
                severity="warning"
            )
            warnings.append(warn.message)
            warning_details.append(warn)
        elif action == "assert":
            if not step.get("assertion") and not step.get("condition"):
                warn = DSLValidationError(
                    message=f"Step {i+1}: 'assert' action should have an 'assertion' field.",
                    line=step_line,
                    severity="warning"
                )
                warnings.append(warn.message)
                warning_details.append(warn)
            else:
                assertion = step.get("assertion") or step.get("condition")
                if isinstance(assertion, dict) and not assertion.get("type"):
                    warn = DSLValidationError(
                        message=f"Step {i+1}: 'assertion' should have a 'type' field.",
                        line=step_line,
                        severity="warning"
                    )
                    warnings.append(warn.message)
                    warning_details.append(warn)
                if isinstance(assertion, dict) and not assertion.get("expected"):
                    warn = DSLValidationError(
                        message=f"Step {i+1}: 'assertion' should have an 'expected' field.",
                        line=step_line,
                        severity="warning"
                    )
                    warnings.append(warn.message)
                    warning_details.append(warn)
        elif action == "click" and not step.get("target"):
            warn = DSLValidationError(
                message=f"Step {i+1}: 'click' action should have a 'target' field.",
                line=step_line,
                severity="warning"
            )
            warnings.append(warn.message)
            warning_details.append(warn)
        elif action == "type":
            if not step.get("text"):
                warn = DSLValidationError(
                    message=f"Step {i+1}: 'type' action should have a 'text' field.",
                    line=step_line,
                    severity="warning"
                )
                warnings.append(warn.message)
                warning_details.append(warn)
            if not step.get("target"):
                warn = DSLValidationError(
                    message=f"Step {i+1}: 'type' action should have a 'target' field.",
                    line=step_line,
                    severity="warning"
                )
                warnings.append(warn.message)
                warning_details.append(warn)

    return len(errors) == 0, errors, warnings, step_count, error_details, warning_details


@router.post("/validate-dsl", response_model=DSLValidationResponse)
async def validate_dsl_endpoint(data: DSLValidationRequest):
    """Validate DSL content without creating a case."""
    valid, errors, warnings, step_count, error_details, warning_details = validate_dsl(data.dsl_content)
    return DSLValidationResponse(
        valid=valid,
        errors=errors,
        error_details=error_details,
        warnings=warnings,
        warning_details=warning_details,
        step_count=step_count
    )


class CaseResponse(BaseModel):
    """Response model for a test case."""
    id: str
    name: str
    source: Optional[str]
    execution_mode: str
    created_at: str


@router.get("/project/{project_id}")
async def list_cases(project_id: str):
    """List all test cases for a project."""
    from opentester.core.storage import get_project_storage
    import json

    storage = get_project_storage()
    project = storage.get_project(project_id)

    if not project:
        raise HTTPException(status_code=404, detail="Project not found")

    cases = []
    for case_id, case in project.cases.items():
        # Handle both dict and TestCase object
        if isinstance(case, dict):
            cases.append({
                "id": case_id,
                "name": case.get("name", "Unnamed"),
                "description": case.get("description", ""),
                "execution_mode": case.get("execution_mode", "screen"),
                "step_count": len(case.get("steps", [])),
                "created_at": case.get("created_at", ""),
                "updated_at": case.get("updated_at", ""),
            })
        else:
            cases.append({
                "id": case_id,
                "name": case.name,
                "description": case.description or "",
                "execution_mode": case.execution_mode,
                "step_count": len(case.steps),
                "created_at": case.created_at,
                "updated_at": case.updated_at,
            })

    return sorted(cases, key=lambda x: x["created_at"], reverse=True)


@router.post("/project/{project_id}")
async def create_case(project_id: str, data: CaseCreate):
    """Create a new test case."""
    from datetime import datetime
    import uuid
    from opentester.core.storage import get_project_storage
    from opentester.models.case import TestCase
    from opentester.models.dsl import TestStep

    storage = get_project_storage()
    project = storage.get_project(project_id)

    if not project:
        raise HTTPException(status_code=404, detail="Project not found")

    # Validate DSL if provided (log warnings but don't block creation)
    dsl_validation_errors = []
    if data.dsl_content:
        is_valid, errors, warnings, step_count = validate_dsl(data.dsl_content)
        if not is_valid:
            # Log validation errors but allow creation for AI-generated drafts
            print(f"DSL validation warnings for new case: {errors}")
            dsl_validation_errors = errors
        if warnings:
            print(f"DSL validation warnings: {warnings}")

    # Parse DSL to get steps
    steps = []
    if data.dsl_content:
        try:
            dsl_data = yaml.safe_load(data.dsl_content)
            if dsl_data:
                raw_steps = dsl_data.get("steps", [])
                # Convert dict steps to TestStep models
                for raw_step in raw_steps:
                    if isinstance(raw_step, dict):
                        try:
                            steps.append(TestStep(**raw_step))
                        except Exception as e:
                            # If validation fails, store as dict for flexibility
                            print(f"TestStep validation warning: {e}")
                            steps.append(raw_step)
                    else:
                        steps.append(raw_step)
        except Exception as e:
            print(f"DSL parsing warning: {e}")
            pass

    # Create test case
    case_id = str(uuid.uuid4())
    now = datetime.now().isoformat()

    test_case = TestCase(
        id=case_id,
        name=data.name,
        description=data.description or "",
        execution_mode=data.execution_mode,
        steps=steps,
        dsl_content=data.dsl_content or "",
        created_at=now,
        updated_at=now,
    )

    # Add to project
    project.cases[case_id] = test_case
    project.updated_at = datetime.now()
    storage.save_project(project)

    return {
        "id": case_id,
        "name": test_case.name,
        "description": test_case.description,
        "execution_mode": test_case.execution_mode,
        "step_count": len(test_case.steps),
        "created_at": test_case.created_at,
        "updated_at": test_case.updated_at,
    }


@router.get("/{case_id}")
async def get_case(case_id: str):
    """Get a test case by ID."""
    from opentester.core.storage import get_project_storage

    storage = get_project_storage()

    # Search through all projects to find the case
    for project_file in storage.settings.PROJECTS_DIR.glob("*.json"):
        try:
            import json
            with open(project_file, "r", encoding="utf-8") as f:
                project_data = json.load(f)

            cases = project_data.get("cases", {})
            if case_id in cases:
                case = cases[case_id]
                return {
                    "id": case_id,
                    "name": case.get("name", "Unnamed"),
                    "description": case.get("description", ""),
                    "execution_mode": case.get("execution_mode", "screen"),
                    "step_count": len(case.get("steps", [])),
                    "dsl_content": case.get("dsl_content", ""),
                    "created_at": case.get("created_at", ""),
                    "updated_at": case.get("updated_at", ""),
                }
        except (json.JSONDecodeError, IOError):
            continue

    raise HTTPException(status_code=404, detail="Case not found")


@router.put("/{case_id}")
async def update_case(case_id: str, data: Dict[str, Any]):
    """Update a test case."""
    from datetime import datetime
    from opentester.core.storage import get_project_storage
    from opentester.models.case import TestCase
    from opentester.models.dsl import TestStep

    storage = get_project_storage()

    # Find the case and its project
    for project_file in storage.settings.PROJECTS_DIR.glob("*.json"):
        try:
            import json
            with open(project_file, "r", encoding="utf-8") as f:
                project_data = json.load(f)

            cases = project_data.get("cases", {})
            if case_id in cases:
                case = cases[case_id]

                # Parse DSL if provided
                steps = []
                dsl_content = data.get("dsl_content", case.get("dsl_content", ""))
                if dsl_content:
                    try:
                        dsl_data = yaml.safe_load(dsl_content)
                        if dsl_data:
                            raw_steps = dsl_data.get("steps", [])
                            for raw_step in raw_steps:
                                if isinstance(raw_step, dict):
                                    try:
                                        steps.append(TestStep(**raw_step))
                                    except Exception:
                                        steps.append(raw_step)
                                else:
                                    steps.append(raw_step)
                    except Exception:
                        pass

                # Update case fields
                case["name"] = data.get("name", case.get("name", "Unnamed"))
                case["description"] = data.get("description", case.get("description", ""))
                case["execution_mode"] = data.get("execution_mode", case.get("execution_mode", "screen"))
                case["dsl_content"] = dsl_content
                case["steps"] = [s.model_dump() if isinstance(s, TestStep) else s for s in steps]
                case["updated_at"] = datetime.now().isoformat()

                # Save project
                with open(project_file, "w", encoding="utf-8") as f:
                    json.dump(project_data, f, ensure_ascii=False, indent=2)

                return {
                    "id": case_id,
                    "name": case["name"],
                    "description": case["description"],
                    "execution_mode": case["execution_mode"],
                    "step_count": len(case["steps"]),
                    "dsl_content": case["dsl_content"],
                    "created_at": case.get("created_at", ""),
                    "updated_at": case["updated_at"],
                }
        except (json.JSONDecodeError, IOError):
            continue

    raise HTTPException(status_code=404, detail="Case not found")


@router.delete("/{case_id}")
async def delete_case(case_id: str):
    """Delete a test case."""
    from opentester.core.storage import get_project_storage
    from datetime import datetime

    storage = get_project_storage()

    # Search through all projects to find and delete the case
    for project_file in storage.settings.PROJECTS_DIR.glob("*.json"):
        try:
            import json
            with open(project_file, "r", encoding="utf-8") as f:
                project_data = json.load(f)

            cases = project_data.get("cases", {})
            if case_id in cases:
                # Remove case from project
                del cases[case_id]
                project_data["updated_at"] = datetime.now().isoformat()

                # Save updated project
                with open(project_file, "w", encoding="utf-8") as f:
                    json.dump(project_data, f, ensure_ascii=False, indent=2)

                return {"message": "Case deleted", "id": case_id}
        except (json.JSONDecodeError, IOError):
            continue

    raise HTTPException(status_code=404, detail="Case not found")


class BatchDeleteRequest(BaseModel):
    """Request model for batch deleting test cases."""
    case_ids: List[str]


@router.post("/batch-delete")
async def batch_delete_cases(data: BatchDeleteRequest):
    """Batch delete test cases."""
    from opentester.core.storage import get_project_storage
    from datetime import datetime

    storage = get_project_storage()
    deleted = []
    not_found = []

    for case_id in data.case_ids:
        found = False
        # Search through all projects to find and delete the case
        for project_file in storage.settings.PROJECTS_DIR.glob("*.json"):
            try:
                import json
                with open(project_file, "r", encoding="utf-8") as f:
                    project_data = json.load(f)

                cases = project_data.get("cases", {})
                if case_id in cases:
                    # Remove case from project
                    del cases[case_id]
                    project_data["updated_at"] = datetime.now().isoformat()

                    # Save updated project
                    with open(project_file, "w", encoding="utf-8") as f:
                        json.dump(project_data, f, ensure_ascii=False, indent=2)

                    deleted.append(case_id)
                    found = True
                    break
            except (json.JSONDecodeError, IOError):
                continue

        if not found:
            not_found.append(case_id)

    return {
        "message": f"Deleted {len(deleted)} cases",
        "deleted": deleted,
        "not_found": not_found,
    }
