"""
RWL-to-OpenWork Task Dispatcher
===============================
Dispatches RWL story tasks to OpenWork for computer-use actions.

Features:
- Convert RWL stories to OpenWork action sequences
- Monitor task completion and report back to RWL
- Handle approval workflows
- Parallel task execution

Usage:
    from loop.openwork_task_dispatcher import OpenWorkTaskDispatcher

    dispatcher = OpenWorkTaskDispatcher(bridge)
    await dispatcher.dispatch_story(story)

Author: Genesis System
Version: 1.0.0
"""

import os
import sys
import json
import asyncio
import logging
import re
from datetime import datetime
from pathlib import Path
from typing import Optional, Dict, Any, List, Callable, Awaitable
from dataclasses import dataclass, field
from enum import Enum, auto

# Add genesis path
GENESIS_ROOT = Path(__file__).parent.parent
sys.path.insert(0, str(GENESIS_ROOT))

# Configure logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)


class TaskType(Enum):
    """Types of RWL tasks that can be dispatched to OpenWork."""
    FILE_OPERATION = auto()
    BROWSER_AUTOMATION = auto()
    DOCUMENT_GENERATION = auto()
    DATA_COLLECTION = auto()
    SCREENSHOT = auto()
    FORM_FILLING = auto()
    API_INTERACTION = auto()
    CODE_EXECUTION = auto()


@dataclass
class OpenWorkTask:
    """A task to be executed by OpenWork."""
    task_id: str
    story_id: str
    task_type: TaskType
    description: str
    actions: List[Dict[str, Any]] = field(default_factory=list)
    dependencies: List[str] = field(default_factory=list)
    status: str = "pending"  # pending, in_progress, completed, failed
    result: Optional[Dict[str, Any]] = None
    error: Optional[str] = None
    created_at: datetime = field(default_factory=datetime.utcnow)
    started_at: Optional[datetime] = None
    completed_at: Optional[datetime] = None

    def to_dict(self) -> Dict[str, Any]:
        return {
            "task_id": self.task_id,
            "story_id": self.story_id,
            "task_type": self.task_type.name,
            "description": self.description,
            "actions": self.actions,
            "dependencies": self.dependencies,
            "status": self.status,
            "result": self.result,
            "error": self.error,
            "created_at": self.created_at.isoformat(),
            "started_at": self.started_at.isoformat() if self.started_at else None,
            "completed_at": self.completed_at.isoformat() if self.completed_at else None
        }


class StoryToTaskConverter:
    """Convert RWL stories to OpenWork tasks."""

    # Pattern matchers for story descriptions
    TASK_PATTERNS = [
        # File operations
        (r"create\s+(?:a\s+)?(?:new\s+)?file\s+(?:at\s+)?(.+)",
         TaskType.FILE_OPERATION,
         lambda m: [{"action": "file_create", "path": m.group(1).strip()}]),

        (r"write\s+(?:content\s+)?to\s+(.+)",
         TaskType.FILE_OPERATION,
         lambda m: [{"action": "file_write", "path": m.group(1).strip()}]),

        (r"read\s+(?:the\s+)?(?:file\s+)?(.+)",
         TaskType.FILE_OPERATION,
         lambda m: [{"action": "file_read", "path": m.group(1).strip()}]),

        # Browser automation
        (r"(?:open|navigate|go)\s+(?:to\s+)?(?:browser\s+)?(?:and\s+)?(?:visit\s+)?(.+)",
         TaskType.BROWSER_AUTOMATION,
         lambda m: [{"action": "browser_navigate", "url": m.group(1).strip()}]),

        (r"search\s+(?:for\s+)?(.+)\s+(?:on|in)\s+(.+)",
         TaskType.BROWSER_AUTOMATION,
         lambda m: [{"action": "browser_search", "query": m.group(1), "site": m.group(2)}]),

        (r"take\s+(?:a\s+)?screenshot\s+(?:of\s+)?(.+)?",
         TaskType.SCREENSHOT,
         lambda m: [{"action": "screenshot", "target": m.group(1) if m.group(1) else "page"}]),

        # Document generation
        (r"(?:generate|create)\s+(?:a\s+)?document\s+(.+)",
         TaskType.DOCUMENT_GENERATION,
         lambda m: [{"action": "document_create", "type": m.group(1).strip()}]),

        (r"(?:generate|create)\s+(?:a\s+)?report\s+(.+)?",
         TaskType.DOCUMENT_GENERATION,
         lambda m: [{"action": "report_create", "type": m.group(1) if m.group(1) else "general"}]),

        # Data collection
        (r"(?:collect|gather|scrape)\s+(?:data\s+)?(?:from\s+)?(.+)",
         TaskType.DATA_COLLECTION,
         lambda m: [{"action": "data_collect", "source": m.group(1).strip()}]),

        # Form filling
        (r"fill\s+(?:out\s+)?(?:the\s+)?form\s+(.+)?",
         TaskType.FORM_FILLING,
         lambda m: [{"action": "form_fill", "form": m.group(1) if m.group(1) else "current"}]),
    ]

    @classmethod
    def convert_story(cls, story: Dict[str, Any]) -> Optional[OpenWorkTask]:
        """Convert an RWL story to OpenWork task."""
        import uuid

        story_id = story.get("id", "unknown")
        title = story.get("title", "")
        description = story.get("description", title)
        files_to_create = story.get("files_to_create", [])

        # Try to match description to task type
        task_type = None
        actions = []

        desc_lower = description.lower()

        for pattern, ttype, action_extractor in cls.TASK_PATTERNS:
            match = re.search(pattern, desc_lower, re.IGNORECASE)
            if match:
                task_type = ttype
                actions = action_extractor(match)
                break

        # Check for file creation hints
        if files_to_create and not task_type:
            task_type = TaskType.FILE_OPERATION
            actions = [
                {"action": "file_create", "path": f}
                for f in files_to_create
            ]

        # Default to code execution for unmatched stories
        if not task_type:
            # Check if it looks like a coding task
            coding_keywords = ["implement", "build", "create", "add", "fix", "update", "refactor"]
            if any(kw in desc_lower for kw in coding_keywords):
                return None  # Let RWL handle coding tasks

            task_type = TaskType.CODE_EXECUTION
            actions = [{"action": "execute", "description": description}]

        task = OpenWorkTask(
            task_id=str(uuid.uuid4()),
            story_id=story_id,
            task_type=task_type,
            description=description,
            actions=actions,
            dependencies=story.get("dependencies", [])
        )

        logger.info(f"Converted story {story_id} to OpenWork task: {task_type.name}")
        return task

    @classmethod
    def is_openwork_suitable(cls, story: Dict[str, Any]) -> bool:
        """Check if a story is suitable for OpenWork execution."""
        description = (story.get("description", "") + " " + story.get("title", "")).lower()

        # OpenWork-suitable keywords
        openwork_keywords = [
            "browser", "website", "screenshot", "file", "folder",
            "document", "report", "form", "collect", "gather",
            "scrape", "navigate", "open", "download", "upload"
        ]

        # Not suitable for OpenWork (should use RWL/Claude Code)
        coding_keywords = [
            "implement", "code", "function", "class", "module",
            "refactor", "test", "debug", "fix bug", "api endpoint",
            "database", "schema", "migration"
        ]

        has_openwork_keyword = any(kw in description for kw in openwork_keywords)
        has_coding_keyword = any(kw in description for kw in coding_keywords)

        return has_openwork_keyword and not has_coding_keyword


class OpenWorkTaskDispatcher:
    """
    Dispatches RWL tasks to OpenWork for execution.

    Connects RWL story execution with OpenWork's computer-use capabilities.
    """

    def __init__(
        self,
        openwork_bridge=None,
        max_concurrent_tasks: int = 3,
        task_timeout_seconds: int = 300
    ):
        self._bridge = openwork_bridge
        self.max_concurrent = max_concurrent_tasks
        self.task_timeout = task_timeout_seconds

        # Task tracking
        self._pending_tasks: Dict[str, OpenWorkTask] = {}
        self._running_tasks: Dict[str, OpenWorkTask] = {}
        self._completed_tasks: Dict[str, OpenWorkTask] = {}

        # Task queue
        self._task_queue: asyncio.Queue = asyncio.Queue()

        # Callbacks
        self._on_task_complete: Optional[Callable[[OpenWorkTask], Awaitable[None]]] = None
        self._on_task_failed: Optional[Callable[[OpenWorkTask], Awaitable[None]]] = None

        # Background worker
        self._running = False
        self._worker_task = None

        logger.info("OpenWorkTaskDispatcher initialized")

    def set_bridge(self, bridge):
        """Set the OpenWork bridge."""
        self._bridge = bridge

    async def start(self):
        """Start the dispatcher."""
        if self._running:
            return

        self._running = True
        self._worker_task = asyncio.create_task(self._process_queue())
        logger.info("OpenWorkTaskDispatcher started")

    async def stop(self):
        """Stop the dispatcher."""
        self._running = False
        if self._worker_task:
            self._worker_task.cancel()
            try:
                await self._worker_task
            except asyncio.CancelledError:
                pass
        logger.info("OpenWorkTaskDispatcher stopped")

    async def dispatch_story(self, story: Dict[str, Any]) -> Optional[OpenWorkTask]:
        """
        Dispatch an RWL story to OpenWork.

        Args:
            story: RWL story dict with id, title, description, etc.

        Returns:
            OpenWorkTask if dispatched, None if not suitable for OpenWork
        """
        # Check if suitable for OpenWork
        if not StoryToTaskConverter.is_openwork_suitable(story):
            logger.info(f"Story {story.get('id')} not suitable for OpenWork")
            return None

        # Convert to task
        task = StoryToTaskConverter.convert_story(story)
        if not task:
            return None

        # Add to queue
        self._pending_tasks[task.task_id] = task
        await self._task_queue.put(task)

        logger.info(f"Story {story.get('id')} dispatched as task {task.task_id}")
        return task

    async def dispatch_tasks_from_board(
        self,
        task_board_path: str
    ) -> List[OpenWorkTask]:
        """Dispatch tasks from an RWL task board JSON file."""
        tasks_dispatched = []

        try:
            with open(task_board_path) as f:
                board = json.load(f)

            stories = board.get("stories", [])

            for story in stories:
                # Skip completed stories
                if story.get("passes", False):
                    continue

                task = await self.dispatch_story(story)
                if task:
                    tasks_dispatched.append(task)

            logger.info(f"Dispatched {len(tasks_dispatched)} tasks from {task_board_path}")

        except Exception as e:
            logger.error(f"Failed to dispatch from board: {e}")

        return tasks_dispatched

    async def _process_queue(self):
        """Process task queue."""
        while self._running:
            try:
                # Wait for capacity
                while len(self._running_tasks) >= self.max_concurrent:
                    await asyncio.sleep(0.5)

                # Get next task
                task = await asyncio.wait_for(
                    self._task_queue.get(),
                    timeout=1.0
                )

                # Execute task
                asyncio.create_task(self._execute_task(task))

            except asyncio.TimeoutError:
                continue
            except asyncio.CancelledError:
                break
            except Exception as e:
                logger.error(f"Queue processing error: {e}")

    async def _execute_task(self, task: OpenWorkTask):
        """Execute a single task through OpenWork."""
        task.status = "in_progress"
        task.started_at = datetime.utcnow()
        self._running_tasks[task.task_id] = task

        if task.task_id in self._pending_tasks:
            del self._pending_tasks[task.task_id]

        try:
            if not self._bridge:
                raise Exception("OpenWork bridge not configured")

            # Execute each action in the task
            results = []
            for action in task.actions:
                action_result = await self._execute_action(action)
                results.append(action_result)

                # Stop on failure
                if not action_result.get("success", False):
                    raise Exception(action_result.get("error", "Action failed"))

            # Task completed
            task.status = "completed"
            task.result = {"actions": results}
            task.completed_at = datetime.utcnow()

            logger.info(f"Task {task.task_id} completed successfully")

            if self._on_task_complete:
                await self._on_task_complete(task)

        except asyncio.TimeoutError:
            task.status = "failed"
            task.error = "Task timed out"
            logger.error(f"Task {task.task_id} timed out")

            if self._on_task_failed:
                await self._on_task_failed(task)

        except Exception as e:
            task.status = "failed"
            task.error = str(e)
            task.completed_at = datetime.utcnow()
            logger.error(f"Task {task.task_id} failed: {e}")

            if self._on_task_failed:
                await self._on_task_failed(task)

        finally:
            # Move to completed
            del self._running_tasks[task.task_id]
            self._completed_tasks[task.task_id] = task

    async def _execute_action(self, action: Dict[str, Any]) -> Dict[str, Any]:
        """Execute a single action through OpenWork bridge."""
        action_type = action.get("action", "unknown")

        # Map to bridge action types
        action_type_map = {
            "file_create": "file_operation",
            "file_write": "file_operation",
            "file_read": "file_operation",
            "browser_navigate": "browser_task",
            "browser_search": "browser_task",
            "screenshot": "browser_task",
            "document_create": "document_automation",
            "report_create": "document_automation",
            "data_collect": "browser_task",
            "form_fill": "browser_task",
            "execute": "system_command"
        }

        bridge_type = action_type_map.get(action_type, "system_command")

        # Create completion future
        result_future = asyncio.Future()

        async def on_complete(bridge_action):
            if not result_future.done():
                result_future.set_result({
                    "success": bridge_action.status.value == "completed",
                    "result": bridge_action.result,
                    "error": bridge_action.error
                })

        # Send to bridge
        await self._bridge.send_action(
            action_type=bridge_type,
            payload=action,
            callback=on_complete
        )

        # Wait for completion with timeout
        try:
            result = await asyncio.wait_for(
                result_future,
                timeout=self.task_timeout
            )
            return result
        except asyncio.TimeoutError:
            return {"success": False, "error": "Action timed out"}

    def on_task_complete(self, callback: Callable[[OpenWorkTask], Awaitable[None]]):
        """Register callback for task completion."""
        self._on_task_complete = callback

    def on_task_failed(self, callback: Callable[[OpenWorkTask], Awaitable[None]]):
        """Register callback for task failure."""
        self._on_task_failed = callback

    def get_status(self) -> Dict[str, Any]:
        """Get dispatcher status."""
        return {
            "running": self._running,
            "pending_tasks": len(self._pending_tasks),
            "running_tasks": len(self._running_tasks),
            "completed_tasks": len(self._completed_tasks),
            "queue_size": self._task_queue.qsize()
        }

    def get_pending_tasks(self) -> List[Dict[str, Any]]:
        """Get pending tasks."""
        return [t.to_dict() for t in self._pending_tasks.values()]

    def get_running_tasks(self) -> List[Dict[str, Any]]:
        """Get running tasks."""
        return [t.to_dict() for t in self._running_tasks.values()]

    def get_completed_tasks(self, limit: int = 50) -> List[Dict[str, Any]]:
        """Get completed tasks."""
        tasks = list(self._completed_tasks.values())
        tasks.sort(key=lambda t: t.completed_at or datetime.min, reverse=True)
        return [t.to_dict() for t in tasks[:limit]]


# CLI for testing
async def main():
    """CLI for testing the dispatcher."""
    import argparse

    parser = argparse.ArgumentParser(description="OpenWork Task Dispatcher")
    parser.add_argument("--story", type=str, help="JSON story to dispatch")
    parser.add_argument("--board", type=str, help="Task board JSON file")
    parser.add_argument("--test", action="store_true", help="Run tests")
    args = parser.parse_args()

    if args.test:
        print("Testing story-to-task conversion...\n")

        test_stories = [
            {"id": "OW-001", "title": "Open GitHub and take screenshot", "description": "Navigate to github.com and take a screenshot"},
            {"id": "OW-002", "title": "Create config file", "description": "Create a new file at config/test.json"},
            {"id": "OW-003", "title": "Collect data from website", "description": "Gather data from example.com/api"},
            {"id": "OW-004", "title": "Implement new feature", "description": "Implement a new authentication module"},
            {"id": "OW-005", "title": "Generate report", "description": "Generate a report of system health"},
        ]

        for story in test_stories:
            suitable = StoryToTaskConverter.is_openwork_suitable(story)
            task = StoryToTaskConverter.convert_story(story) if suitable else None

            print(f"Story: {story['id']} - {story['title']}")
            print(f"  Suitable for OpenWork: {suitable}")
            if task:
                print(f"  Task Type: {task.task_type.name}")
                print(f"  Actions: {task.actions}")
            print()

        return

    if args.story:
        story = json.loads(args.story)
        task = StoryToTaskConverter.convert_story(story)
        if task:
            print(f"Converted task:")
            print(json.dumps(task.to_dict(), indent=2))
        else:
            print("Story not suitable for OpenWork")
        return

    if args.board:
        dispatcher = OpenWorkTaskDispatcher()
        # Note: Would need bridge for actual execution
        print(f"Would dispatch tasks from: {args.board}")
        return

    parser.print_help()


if __name__ == "__main__":
    asyncio.run(main())
