"""Task delegation service: lifecycle, modes, feedback.

Tasks are always scoped to a project. Files live at
    agntspace-projects/{project_slug}/tasks/{task_slug}.md

When a task is created without a project slug, it lands in the default
"personal" project, which is auto-created on first use.
"""

from __future__ import annotations

import logging
import re
from datetime import UTC, datetime

from agntspace.vault.reader import VaultDocument, VaultReader
from agntspace.vault.writer import VaultWriter

log = logging.getLogger(__name__)

STATUSES = ("delegated", "in_progress", "blocked", "review", "done", "cancelled")
MODES = ("jdi", "review", "recommend", "watch")

DEFAULT_PROJECT_SLUG = "personal"

_TASK_TEMPLATE = """---
title: "{title}"
entity_type: task
status: delegated
mode: {mode}
domain: {domain}
project: {project}
assigned_agent: {assigned_agent}
delegated: "{delegated}"
due: ""
---

## Brief
{brief}

## Steps

## Progress Log
- {timestamp} — Task delegated.

## Escalation

## Result

## Feedback
"""

_PERSONAL_PROJECT_TEMPLATE = """---
title: "Personal"
entity_type: project
status: active
created: "{created}"
target_date: ""
related_goals: []
assigned_agents: []
linked_kbs: []
---

## Description

Default container for standalone tasks that aren't tied to a specific project.

## Milestones


## Log
- {timestamp} — Personal project created.
"""


class TaskService:
    """Manages project-scoped task files under agntspace-projects/{slug}/tasks/."""

    def __init__(self, vault_reader: VaultReader, vault_writer: VaultWriter) -> None:
        self._reader = vault_reader
        self._writer = vault_writer
        # Post-wired in main.py — lets task creation reflect in the graph
        # immediately via a structural triple refresh.
        self._graph = None  # type: ignore[assignment]

    def _refresh_graph(self) -> None:
        if self._graph is not None:
            try:
                self._graph.refresh_structural_triples()
            except Exception as exc:  # pragma: no cover
                log.debug("Graph structural refresh failed: %s", exc)

    # --- public API ---

    def create_task(
        self,
        title: str,
        brief: str,
        mode: str = "review",
        domain: str = "general",
        project: str | None = None,
        assigned_agent: str | None = None,
    ) -> str:
        """Create a new task file inside a project. Returns the relative vault path.

        If ``project`` is not given, the default ``personal`` project is used
        and created on-demand. ``assigned_agent`` is the key of the specialist
        that owns execution — surfaces in the graph as (task, assigned_to, agent).
        """
        project_slug = self._slugify(project) if project else DEFAULT_PROJECT_SLUG
        self._ensure_project(project_slug)

        task_slug = self._slugify(title)
        path = f"projects/{project_slug}/tasks/{task_slug}.md"
        now = datetime.now(UTC)
        content = _TASK_TEMPLATE.format(
            title=title,
            mode=mode,
            domain=domain,
            project=project_slug,
            assigned_agent=assigned_agent or "",
            delegated=now.isoformat(),
            brief=brief,
            timestamp=now.strftime("%Y-%m-%d %H:%M"),
        )
        self._writer.write(path, content, contract_action="create_task")
        self._refresh_graph()
        return path

    def get_task(self, slug: str, project: str | None = None) -> VaultDocument | None:
        """Get a task by slug. Searches all projects unless ``project`` is given."""
        path = self._find_task_path(slug, project)
        if path is None:
            return None
        return self._reader.read(path)

    def list_tasks(
        self,
        status_filter: str | None = None,
        project: str | None = None,
    ) -> list[dict]:
        """List tasks across all projects (or one specific project)."""
        files = self._list_task_files(project)
        tasks = []
        for path in files:
            try:
                doc = self._reader.read(path)
            except (FileNotFoundError, OSError):
                continue
            meta = doc.metadata
            task_status = meta.get("status", "unknown")
            if status_filter and task_status != status_filter:
                continue
            # project slug is the parent-of-parent of the task file
            # projects/{project_slug}/tasks/{task_slug}.md
            parts = path.split("/")
            project_slug = parts[1] if len(parts) >= 4 else meta.get("project", "")
            updated_at = ""
            try:
                paths = getattr(self._reader, "paths", None)
                if paths is not None:
                    physical = paths.resolve(path)
                    updated_at = datetime.fromtimestamp(
                        physical.stat().st_mtime, UTC
                    ).isoformat()
            except OSError:
                updated_at = ""
            tasks.append({
                "slug": parts[-1].removesuffix(".md"),
                "title": meta.get("title", parts[-1].removesuffix(".md")),
                "status": task_status,
                "mode": meta.get("mode", "review"),
                "domain": meta.get("domain", ""),
                "project": project_slug,
                "delegated": meta.get("delegated", ""),
                "due": meta.get("due", ""),
                "updated_at": updated_at,
            })
        return tasks

    def update_status(self, slug: str, new_status: str, project: str | None = None) -> None:
        """Update a task's status."""
        if new_status not in STATUSES:
            raise ValueError(f"Invalid status: {new_status}")
        self._update_metadata(slug, "status", new_status, project)
        self._append_progress(slug, f"Status changed to {new_status}.", project)

    def add_progress(self, slug: str, note: str, project: str | None = None) -> None:
        """Add a progress log entry."""
        self._append_progress(slug, note, project)

    def rename_task(
        self, slug: str, new_title: str, project: str | None = None
    ) -> dict:
        """Rename a task: rewrite title and move file if slug changes."""
        old_path = self._find_task_path(slug, project)
        if old_path is None:
            raise FileNotFoundError(f"Task not found: {slug}")

        new_slug = self._slugify(new_title)
        if not new_slug:
            raise ValueError("New title produces an empty slug")

        project_slug = old_path.split("/")[1]
        doc = self._reader.read(old_path)
        content = re.sub(
            r'^title:\s*".*"',
            f'title: "{new_title}"',
            doc.raw,
            count=1,
            flags=re.MULTILINE,
        )

        if new_slug == slug:
            self._writer.write(old_path, content, contract_action="create_task")
            self._refresh_graph()
            return {"slug": slug, "project": project_slug}

        new_path = f"projects/{project_slug}/tasks/{new_slug}.md"
        if self._reader.exists(new_path):
            raise ValueError(f"Task already exists: {new_slug}")

        self._writer.write(new_path, content, contract_action="create_task")

        try:
            old_physical = (
                self._reader.paths.resolve(old_path)
                if self._reader.paths
                else self._reader._vault_dir / old_path  # noqa: SLF001
            )
            if old_physical.is_file():
                old_physical.unlink()
        except Exception:
            pass

        self._refresh_graph()
        return {"slug": new_slug, "project": project_slug}

    def reassign_project(
        self, slug: str, new_project: str, current_project: str | None = None
    ) -> tuple[str, str]:
        """Move a task from one project to another.

        Returns (old_project_slug, new_path).
        """
        old_path = self._find_task_path(slug, current_project)
        if old_path is None:
            raise FileNotFoundError(f"Task not found: {slug}")

        old_project_slug = old_path.split("/")[1]
        new_project_slug = self._slugify(new_project)
        if new_project_slug == old_project_slug:
            return old_project_slug, old_path

        # New project must already exist (except the default personal fallback).
        self._ensure_project(new_project_slug)

        doc = self._reader.read(old_path)
        content = re.sub(
            r"^project:\s*.*$",
            f"project: {new_project_slug}",
            doc.raw,
            count=1,
            flags=re.MULTILINE,
        )
        new_path = f"projects/{new_project_slug}/tasks/{slug}.md"
        self._writer.write(new_path, content, contract_action="create_task")
        self._append_progress(
            slug,
            f"Reassigned from '{old_project_slug}' to '{new_project_slug}'.",
            new_project_slug,
        )

        # Delete the old file from disk
        try:
            old_physical = (
                self._reader.paths.resolve(old_path)
                if self._reader.paths
                else self._reader._vault_dir / old_path  # noqa: SLF001
            )
            if old_physical.is_file():
                old_physical.unlink()
        except Exception:
            pass

        self._refresh_graph()
        return old_project_slug, new_path

    def set_feedback(
        self,
        slug: str,
        rating: str,
        note: str = "",
        project: str | None = None,
    ) -> dict:
        """Set feedback on a completed task. Returns feedback data for trust updates."""
        if rating not in ("good", "needs_improvement", "redo"):
            raise ValueError(f"Invalid rating: {rating}")

        path = self._find_task_path(slug, project)
        if path is None:
            raise FileNotFoundError(f"Task not found: {slug}")

        doc = self._reader.read(path)
        domain = doc.metadata.get("domain", "general")

        feedback_text = f"Rating: {rating}"
        if note:
            feedback_text += f'\nNote: "{note}"'

        content = doc.raw
        content = content.replace(
            "## Feedback\n",
            f"## Feedback\n{feedback_text}\n",
        )

        # Also mark as done if not already
        if doc.metadata.get("status") != "done":
            content = re.sub(
                r"^status:\s*\S+",
                "status: done",
                content,
                count=1,
                flags=re.MULTILINE,
            )

        self._writer.write(path, content, contract_action="create_task")

        return {"slug": slug, "domain": domain, "rating": rating, "note": note}

    def delete_task(self, slug: str, project: str | None = None) -> str:
        """Delete a task file. Returns the project slug it belonged to."""
        path = self._find_task_path(slug, project)
        if path is None:
            raise FileNotFoundError(f"Task not found: {slug}")
        project_slug = path.split("/")[1]
        physical = (
            self._reader.paths.resolve(path)
            if self._reader.paths
            else self._reader._vault_dir / path  # noqa: SLF001
        )
        if physical.is_file():
            physical.unlink()
        self._refresh_graph()
        return project_slug

    def infer_mode(self, text: str) -> str:
        """Infer delegation mode from natural language phrasing."""
        lower = text.lower()
        if any(p in lower for p in ["let me review", "show me first", "check with me"]):
            return "review"
        if any(p in lower for p in ["what are my options", "recommend", "research"]):
            return "recommend"
        if any(p in lower for p in ["let me know when", "watch", "alert me", "notify me"]):
            return "watch"
        return "jdi"

    # --- internals ---

    def _ensure_project(self, project_slug: str) -> None:
        """Create a project shell if it does not exist. Only auto-creates the default."""
        project_file = f"projects/{project_slug}/PROJECT.md"
        if self._reader.exists(project_file):
            return
        if project_slug != DEFAULT_PROJECT_SLUG:
            # Non-default projects must be created explicitly via ProjectService.
            raise FileNotFoundError(
                f"Project '{project_slug}' does not exist. Create it first."
            )
        now = datetime.now(UTC)
        content = _PERSONAL_PROJECT_TEMPLATE.format(
            created=now.strftime("%Y-%m-%d"),
            timestamp=now.strftime("%Y-%m-%d %H:%M"),
        )
        self._writer.write(project_file, content, contract_action="create_task")
        # Seed the standard subdirs so the project looks like any other.
        for sub in ("tasks", "notes", "output"):
            self._writer.write(
                f"projects/{project_slug}/{sub}/.gitkeep", "",
                contract_action="create_task",
            )

    def _list_task_files(self, project: str | None) -> list[str]:
        """Return logical paths of task files, optionally scoped to one project."""
        if project:
            project_slug = self._slugify(project)
            files = self._reader.list_files(
                f"projects/{project_slug}/tasks", "*.md"
            )
            return [f"projects/{project_slug}/tasks/{f.name}" for f in files]

        # Scan all projects: projects/*/tasks/*.md
        files = self._reader.list_files("projects", "*/tasks/*.md")
        out: list[str] = []
        for f in files:
            # Reconstruct the logical path from the physical path.
            # f will look like .../agntspace-projects/{slug}/tasks/{task}.md
            parts = f.parts
            try:
                # Find the "tasks" segment and take slug + filename around it.
                idx = len(parts) - 2  # tasks
                if parts[idx] == "tasks":
                    project_slug = parts[idx - 1]
                    out.append(f"projects/{project_slug}/tasks/{f.name}")
            except (IndexError, ValueError):
                continue
        return out

    def _find_task_path(self, slug: str, project: str | None) -> str | None:
        """Locate the logical path for a task slug."""
        if project:
            project_slug = self._slugify(project)
            path = f"projects/{project_slug}/tasks/{slug}.md"
            return path if self._reader.exists(path) else None

        # Scan all projects for the first match.
        for path in self._list_task_files(None):
            if path.endswith(f"/tasks/{slug}.md"):
                return path
        return None

    def _update_metadata(
        self, slug: str, key: str, value: str, project: str | None
    ) -> None:
        path = self._find_task_path(slug, project)
        if path is None:
            raise FileNotFoundError(f"Task not found: {slug}")
        doc = self._reader.read(path)
        content = doc.raw
        content = re.sub(
            rf"^{re.escape(key)}:\s*.*$",
            f"{key}: {value}",
            content,
            count=1,
            flags=re.MULTILINE,
        )
        self._writer.write(path, content, contract_action="create_task")

    def _append_progress(self, slug: str, note: str, project: str | None) -> None:
        path = self._find_task_path(slug, project)
        if path is None:
            raise FileNotFoundError(f"Task not found: {slug}")
        doc = self._reader.read(path)
        content = doc.raw
        from agntspace.infra.timezone import now_local
        timestamp = now_local().strftime("%Y-%m-%d %H:%M")
        line = f"- {timestamp} — {note}"

        pattern = r"(## Progress Log\n(?:.*\n)*?)((?=\n## )|\Z)"
        match = re.search(pattern, content)
        if match:
            insert_at = match.end(1)
            content = content[:insert_at] + line + "\n" + content[insert_at:]
        else:
            content += f"\n## Progress Log\n{line}\n"

        self._writer.write(path, content, contract_action="create_task")

    @staticmethod
    def _slugify(text: str) -> str:
        slug = text.lower().strip()
        slug = re.sub(r"[^\w\s-]", "", slug)
        slug = re.sub(r"[\s_]+", "-", slug)
        slug = re.sub(r"-+", "-", slug)
        return slug[:60].strip("-")
