"""Project service: container for tasks, goals, subagents, and knowledge."""

from __future__ import annotations

import logging
import re
from datetime import UTC, datetime

from agntspace.infra.timezone import local_today, now_local
from agntspace.vault.reader import VaultDocument, VaultReader
from agntspace.vault.writer import VaultWriter

log = logging.getLogger(__name__)

STATUSES = ("active", "on_hold", "completed", "archived")

_PROJECT_TEMPLATE = """---
title: "{title}"
entity_type: project
status: active
created: "{created}"
target_date: "{target_date}"
related_goals: {goals_yaml}
assigned_agents: {agents_yaml}
linked_kbs: {kbs_yaml}
---

## Description

{description}

## Milestones


## Log
- {timestamp} — Project created.
"""


class ProjectService:
    """Manages project files in vault/projects/."""

    def __init__(self, vault_reader: VaultReader, vault_writer: VaultWriter) -> None:
        self._reader = vault_reader
        self._writer = vault_writer
        # Post-wired in main.py so project/goal/task relationships land in the
        # graph the moment a file is written, not on the next full rebuild.
        self._graph = None  # type: ignore[assignment]

    def _refresh_graph(self) -> None:
        """Refresh structural triples if the graph is wired."""
        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)

    def _resolve(self, relative_path: str):
        """Resolve a logical vault path to a physical Path."""
        if self._reader.paths:
            return self._reader.paths.resolve(relative_path)
        return self._reader._vault_dir / relative_path  # noqa: SLF001

    def create_project(
        self,
        title: str,
        description: str = "",
        target_date: str = "",
        related_goals: list[str] | None = None,
        assigned_agents: list[str] | None = None,
        linked_kbs: list[str] | None = None,
    ) -> str:
        """Create a new project. Returns the relative vault path."""
        slug = self._slugify(title)
        path = f"projects/{slug}/PROJECT.md"
        now = datetime.now(UTC)
        content = _PROJECT_TEMPLATE.format(
            title=title,
            created=now.strftime("%Y-%m-%d"),
            target_date=target_date,
            goals_yaml=self._to_yaml_list(related_goals),
            agents_yaml=self._to_yaml_list(assigned_agents),
            kbs_yaml=self._to_yaml_list(linked_kbs),
            description=description or "No description yet.",
            timestamp=now.strftime("%Y-%m-%d %H:%M"),
        )
        self._writer.write(path, content, contract_action="modify_project")
        # Create sub-directories
        for sub in ("tasks", "notes", "output"):
            gitkeep = f"projects/{slug}/{sub}/.gitkeep"
            self._writer.write(gitkeep, "", contract_action="modify_project")
        self._refresh_graph()
        return path

    def get_project(self, slug: str) -> VaultDocument | None:
        """Get a project by slug."""
        path = f"projects/{slug}/PROJECT.md"
        if not self._reader.exists(path):
            return None
        return self._reader.read(path)

    def list_projects(self, status_filter: str | None = None) -> list[dict]:
        """List all projects, optionally filtered by status."""
        projects_dir = self._resolve("projects")
        if not projects_dir.is_dir():
            return []

        results = []
        for child in sorted(projects_dir.iterdir()):
            if not child.is_dir():
                continue
            project_file = child / "PROJECT.md"
            if not project_file.is_file():
                continue
            slug = child.name
            doc = self._reader.read(f"projects/{slug}/PROJECT.md")
            meta = doc.metadata
            status = meta.get("status", "active")
            if status_filter and status != status_filter:
                continue
            try:
                updated_at = datetime.fromtimestamp(
                    project_file.stat().st_mtime, UTC
                ).isoformat()
            except OSError:
                updated_at = ""
            results.append({
                "slug": slug,
                "title": meta.get("title", slug),
                "status": status,
                "created": meta.get("created", ""),
                "target_date": meta.get("target_date", ""),
                "related_goals": meta.get("related_goals", []) or [],
                "assigned_agents": meta.get("assigned_agents", []) or [],
                "linked_kbs": meta.get("linked_kbs", []) or [],
                "updated_at": updated_at,
            })
        return results

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

    def update_project(
        self,
        slug: str,
        title: str | None = None,
        description: str | None = None,
        target_date: str | None = None,
        related_goals: list[str] | None = None,
        assigned_agents: list[str] | None = None,
        linked_kbs: list[str] | None = None,
    ) -> None:
        """Update project fields."""
        path = f"projects/{slug}/PROJECT.md"
        doc = self._reader.read(path)
        content = doc.raw

        if title is not None:
            content = re.sub(
                r'^title:\s*".*"', f'title: "{title}"', content,
                count=1, flags=re.MULTILINE,
            )
        if target_date is not None:
            content = re.sub(
                r'^target_date:\s*".*"', f'target_date: "{target_date}"', content,
                count=1, flags=re.MULTILINE,
            )
        if description is not None:
            content = re.sub(
                r"(## Description\n\n)(.*?)(\n\n## )",
                rf"\g<1>{description}\n\n## ",
                content,
                count=1,
                flags=re.DOTALL,
            )
        if related_goals is not None:
            content = re.sub(
                r"^related_goals:\s*.*$",
                f"related_goals: {self._to_yaml_list(related_goals)}",
                content, count=1, flags=re.MULTILINE,
            )
        if assigned_agents is not None:
            content = re.sub(
                r"^assigned_agents:\s*.*$",
                f"assigned_agents: {self._to_yaml_list(assigned_agents)}",
                content, count=1, flags=re.MULTILINE,
            )
        if linked_kbs is not None:
            content = re.sub(
                r"^linked_kbs:\s*.*$",
                f"linked_kbs: {self._to_yaml_list(linked_kbs)}",
                content, count=1, flags=re.MULTILINE,
            )

        self._writer.write(path, content, contract_action="modify_project")
        # Edge-affecting changes (goals/agents lists, title) need a refresh.
        if related_goals is not None or assigned_agents is not None or title is not None:
            self._refresh_graph()

    def add_milestone(self, slug: str, milestone: str) -> None:
        """Add a milestone to the project."""
        path = f"projects/{slug}/PROJECT.md"
        doc = self._reader.read(path)
        content = doc.raw
        now = local_today()
        line = f"- [ ] {milestone} (added {now})"

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

        self._writer.write(path, content, contract_action="modify_project")
        self._append_log(slug, f"Milestone added: {milestone}")

    def add_log(self, slug: str, note: str) -> None:
        """Add a log entry."""
        self._append_log(slug, note)

    def get_project_detail(self, slug: str) -> dict | None:
        """Get full project detail including content sections."""
        doc = self.get_project(slug)
        if not doc:
            return None
        meta = doc.metadata
        return {
            "slug": slug,
            "title": meta.get("title", slug),
            "status": meta.get("status", "active"),
            "created": meta.get("created", ""),
            "target_date": meta.get("target_date", ""),
            "related_goals": meta.get("related_goals", []) or [],
            "assigned_agents": meta.get("assigned_agents", []) or [],
            "linked_kbs": meta.get("linked_kbs", []) or [],
            "description": self._extract_section(doc.content, "Description"),
            "milestones": self._extract_section(doc.content, "Milestones"),
            "log": self._extract_section(doc.content, "Log"),
        }

    def rename_project(self, slug: str, new_title: str) -> dict:
        """Rename a project: move dir, update title, rewrite task project field."""
        import shutil

        old_dir = self._resolve(f"projects/{slug}")
        if not old_dir.is_dir():
            raise FileNotFoundError(f"Project not found: {slug}")

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

        # Rewrite title
        old_project_file = f"projects/{slug}/PROJECT.md"
        doc = self._reader.read(old_project_file)
        content = re.sub(
            r'^title:\s*".*"',
            f'title: "{new_title}"',
            doc.raw,
            count=1,
            flags=re.MULTILINE,
        )
        self._writer.write(old_project_file, content)

        if new_slug == slug:
            return {"slug": slug, "tasks_updated": 0}

        new_dir = self._resolve(f"projects/{new_slug}")
        if new_dir.exists():
            raise ValueError(f"Project already exists: {new_slug}")

        shutil.move(str(old_dir), str(new_dir))

        # Rewrite task project field in files that moved
        tasks_dir = new_dir / "tasks"
        tasks_updated = 0
        if tasks_dir.is_dir():
            for task_file in tasks_dir.glob("*.md"):
                try:
                    tdoc = self._reader.read(f"projects/{new_slug}/tasks/{task_file.name}")
                    new_content = re.sub(
                        r"^project:\s*.*$",
                        f"project: {new_slug}",
                        tdoc.raw,
                        count=1,
                        flags=re.MULTILINE,
                    )
                    self._writer.write(
                        f"projects/{new_slug}/tasks/{task_file.name}", new_content,
                        contract_action="modify_project",
                    )
                    tasks_updated += 1
                except Exception:
                    continue

        return {"slug": new_slug, "tasks_updated": tasks_updated}

    def delete_project(self, slug: str) -> None:
        """Delete a project directory and clean up KB wiki project pages."""
        import shutil

        # Clean up wiki/projects/{slug}/ in all KBs that may have project pages
        agnt_kb_dir = self._resolve("knowledge")
        if agnt_kb_dir.is_dir():
            for kb_dir in agnt_kb_dir.iterdir():
                if not kb_dir.is_dir():
                    continue
                project_wiki_dir = kb_dir / "wiki" / "projects" / slug
                if project_wiki_dir.is_dir():
                    shutil.rmtree(project_wiki_dir)

        projects_dir = self._resolve(f"projects/{slug}")
        if projects_dir.is_dir():
            shutil.rmtree(projects_dir)

        # Retract structural triples now that the files are gone.
        self._refresh_graph()

    def list_output_files(self, slug: str) -> list[dict]:
        """List files in a project's output/ directory with companion metadata."""
        import frontmatter  # type: ignore[import-untyped]

        out_dir = self._resolve(f"projects/{slug}/output")
        if not out_dir.is_dir():
            return []

        results: list[dict] = []
        seen_companions: set[str] = set()

        for f in sorted(out_dir.iterdir(), key=lambda p: p.stat().st_mtime, reverse=True):
            if f.name.startswith(".") or f.name == ".gitkeep":
                continue
            # Skip companion .md files (they are metadata, not primary outputs)
            if f.suffix == ".md" and f.stem.endswith((".pptx", ".docx", ".xlsx", ".pdf")):
                seen_companions.add(f.name)
                continue

            stat = f.stat()
            entry: dict = {
                "filename": f.name,
                "path": str(f.relative_to(self._reader.paths.root_dir)) if self._reader.paths else f.name,
                "size": stat.st_size,
                "modified": datetime.fromtimestamp(stat.st_mtime, tz=UTC).isoformat(),
                "type": f.suffix.lstrip(".") or "unknown",
            }

            # Read companion .md for metadata if it exists
            companion = f.with_suffix(f.suffix + ".md")
            if companion.exists():
                try:
                    post = frontmatter.load(str(companion))
                    entry["output_type"] = post.get("output_type", "")
                    entry["agent"] = post.get("agent", "")
                    entry["ingested"] = post.get("ingested", False)
                    raw_date = post.get("date", "")
                    entry["date"] = str(raw_date) if raw_date else ""
                    entry["title"] = post.get("title", f.stem)
                    entry["has_companion"] = True
                except Exception:
                    entry["title"] = f.stem
                    entry["has_companion"] = False
            else:
                entry["title"] = f.stem
                entry["has_companion"] = False

            results.append(entry)

        return results

    # ── Internal ──

    def _update_metadata(self, slug: str, key: str, value: str) -> None:
        path = f"projects/{slug}/PROJECT.md"
        doc = self._reader.read(path)
        content = re.sub(
            rf"^{re.escape(key)}:\s*.*$",
            f"{key}: {value}",
            doc.raw, count=1, flags=re.MULTILINE,
        )
        self._writer.write(path, content, contract_action="modify_project")

    def _append_log(self, slug: str, note: str) -> None:
        path = f"projects/{slug}/PROJECT.md"
        doc = self._reader.read(path)
        content = doc.raw
        timestamp = now_local().strftime("%Y-%m-%d %H:%M")
        line = f"- {timestamp} — {note}"

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

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

    @staticmethod
    def _extract_section(content: str, heading: str) -> str:
        pattern = rf"## {re.escape(heading)}\n(.*?)(?=^## |\Z)"
        match = re.search(pattern, content, re.DOTALL | re.MULTILINE)
        return match.group(1).strip() if match else ""

    @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("-")

    @staticmethod
    def _to_yaml_list(items: list[str] | None) -> str:
        if not items:
            return "[]"
        return "[" + ", ".join(f'"{i}"' for i in items) + "]"
