"""Obsidian domain services."""

from __future__ import annotations

import re
from dataclasses import dataclass, field
from datetime import date

from .entities import Canvas, CanvasEdge, CanvasNode, Link, Note, Task, Template, Vault
from .enums import CanvasNodeType, NoteState, TaskState
from .exceptions import (
    InvalidCanvasError,
    InvalidLinkError,
    InvalidNoteError,
    InvalidSearchError,
    InvalidTaskError,
    InvalidTemplateError,
    InvalidVaultError,
)
from .policies import DefaultVaultPolicy, VaultPolicy
from .value_objects import Alias, CanvasName, NotePath, NoteTitle, TagName, TaskText, TemplateName

_WIKILINK_PATTERN = re.compile(r"(!)?\[\[([^\]]+)\]\]")
_TAG_PATTERN = re.compile(r"(?<!\w)#([A-Za-z0-9/_-]+)")


@dataclass(frozen=True)
class DiscoveryResult:
    links: list[Link]
    tags: list[TagName]


@dataclass
class ContentDiscoveryService:
    def discover(self, content: str) -> DiscoveryResult:
        if not isinstance(content, str):
            raise InvalidNoteError("content must be a string")

        links: list[Link] = []
        content_for_tags = _WIKILINK_PATTERN.sub(" ", content)
        tags: list[TagName] = []
        seen_tags: set[str] = set()
        for match in _TAG_PATTERN.finditer(content_for_tags):
            tag = TagName(match.group(1))
            if tag.value not in seen_tags:
                seen_tags.add(tag.value)
                tags.append(tag)

        for match in _WIKILINK_PATTERN.finditer(content):
            is_embed = bool(match.group(1))
            body = match.group(2).strip()
            alias_text = None
            fragment_text = None
            if "|" in body:
                body, alias_text = body.split("|", 1)
            if "#" in body:
                body, fragment_text = body.split("#", 1)
                if fragment_text.startswith("^"):
                    fragment_text = fragment_text[1:]
            target = NotePath(body)
            alias = Alias(alias_text) if alias_text is not None and alias_text.strip() else None
            links.append(Link(target=target, alias=alias, is_embed=is_embed, fragment=fragment_text))

        return DiscoveryResult(links=links, tags=tags)


@dataclass
class VaultService:
    vault_policy: VaultPolicy = field(default_factory=DefaultVaultPolicy)
    discovery_service: ContentDiscoveryService = field(default_factory=ContentDiscoveryService)

    def add_note(self, vault: Vault, note: Note) -> Note:
        if not isinstance(vault, Vault):
            raise InvalidVaultError("vault must be a Vault instance")
        if not isinstance(note, Note):
            raise InvalidNoteError("note must be a Note instance")
        if not self.vault_policy.can_add_note(vault, note):
            raise InvalidVaultError(f"duplicate note path: {note.path.value}")
        return vault.add_note(note)

    def move_note(self, vault: Vault, path: NotePath, new_path: NotePath) -> Note:
        if not isinstance(vault, Vault):
            raise InvalidVaultError("vault must be a Vault instance")
        note = vault.get_note(path)
        if not self.vault_policy.can_move_note(vault, note, new_path):
            raise InvalidVaultError(f"duplicate note path: {new_path.value}")
        return vault.move_note(path, new_path)

    def archive_note(self, vault: Vault, path: NotePath) -> Note:
        if not isinstance(vault, Vault):
            raise InvalidVaultError("vault must be a Vault instance")
        return vault.archive_note(path)

    def trash_note(self, vault: Vault, path: NotePath) -> Note:
        if not isinstance(vault, Vault):
            raise InvalidVaultError("vault must be a Vault instance")
        return vault.trash_note(path)

    def restore_note(self, vault: Vault, path: NotePath) -> Note:
        if not isinstance(vault, Vault):
            raise InvalidVaultError("vault must be a Vault instance")
        note = vault.get_note(path)
        if not self.vault_policy.can_restore_note(note):
            raise InvalidVaultError("note cannot be restored")
        return vault.restore_note(path)

    def backlinks(self, vault: Vault, target: NotePath) -> list[Note]:
        if not isinstance(vault, Vault):
            raise InvalidVaultError("vault must be a Vault instance")
        return vault.backlinks(target)

    def search(self, vault: Vault, query: str) -> list[Note]:
        if not isinstance(vault, Vault):
            raise InvalidVaultError("vault must be a Vault instance")
        if not isinstance(query, str) or not query.strip():
            raise InvalidSearchError("query must be a non-empty string")
        return vault.search(query)

    def sync_note_metadata(self, note: Note) -> Note:
        if not isinstance(note, Note):
            raise InvalidNoteError("note must be a Note instance")
        discovery = self.discovery_service.discover(note.content)
        for tag in discovery.tags:
            note.add_tag(tag)
        for link in discovery.links:
            note.add_link(link)
        return note

    def create_daily_note(self, vault: Vault, on_date: date | None = None) -> Note:
        if not isinstance(vault, Vault):
            raise InvalidVaultError("vault must be a Vault instance")
        on_date = on_date or date.today()
        if not isinstance(on_date, date):
            raise InvalidNoteError("on_date must be a date")
        path = NotePath(f"Daily/{on_date.isoformat()}.md")
        existing = next((note for note in vault.notes if note.path == path), None)
        if existing is not None:
            return existing
        note = Note(
            title=NoteTitle(on_date.isoformat()),
            path=path,
            content="",
            tags=[TagName("daily")],
        )
        return self.add_note(vault, note)


@dataclass
class TemplateService:
    def _format_yaml_value(self, value: object) -> str:
        if value is None:
            return "null"
        if isinstance(value, bool):
            return "true" if value else "false"
        if isinstance(value, (int, float)):
            return str(value)
        text = str(value)
        if text == "":
            return '""'
        if any(ch in text for ch in [":", "#", "\n", '"', "'"]) or text.strip() != text:
            escaped = text.replace('"', '\\"')
            return f'"{escaped}"'
        return text

    def _render_frontmatter(self, frontmatter: dict[str, object]) -> str:
        if not frontmatter:
            return ""
        lines = ["---"]
        for key, value in frontmatter.items():
            lines.append(f"{key}: {self._format_yaml_value(value)}")
        lines.append("---")
        return "\n".join(lines)

    def render(self, template: Template, context: dict[str, object] | None = None) -> str:
        if not isinstance(template, Template):
            raise InvalidTemplateError("template must be a Template instance")
        context = context or {}
        if not isinstance(context, dict):
            raise InvalidTemplateError("context must be a dictionary")

        result = template.body
        for key, value in context.items():
            placeholder = f"{{{{{key}}}}}"
            result = result.replace(placeholder, str(value))
        return result

    def create_note_from_template(
        self,
        template: Template,
        title: NoteTitle,
        path: NotePath,
        context: dict[str, object] | None = None,
        frontmatter: dict[str, object] | None = None,
    ) -> Note:
        if frontmatter is None:
            frontmatter = {}
        if not isinstance(frontmatter, dict):
            raise InvalidTemplateError("frontmatter must be a dictionary")
        merged_frontmatter: dict[str, object] = dict(template.frontmatter)
        merged_frontmatter.update(frontmatter)
        render_context: dict[str, object] = dict(merged_frontmatter)
        if context:
            if not isinstance(context, dict):
                raise InvalidTemplateError("context must be a dictionary")
            render_context.update(context)
        body = self.render(template, context=render_context)
        header = self._render_frontmatter(merged_frontmatter)
        content = f"{header}\n{body}" if header else body
        note = Note(title=title, path=path, content=content, frontmatter=merged_frontmatter)
        TaskService().sync_note_tasks(note)
        return note


@dataclass
class TaskService:
    def create(
        self, text: str | TaskText, *, note_path: NotePath | None = None, tags: list[TagName] | None = None
    ) -> Task:
        if isinstance(text, str):
            text = TaskText(text)
        if not isinstance(text, TaskText):
            raise InvalidTaskError("text must be a TaskText instance or string")
        task = Task(text=text, note_path=note_path, tags=list(tags or []))
        return task

    def complete(self, task: Task, completed_at: date | None = None) -> Task:
        if not isinstance(task, Task):
            raise InvalidTaskError("task must be a Task instance")
        task.complete(completed_at=completed_at)
        return task

    def reopen(self, task: Task) -> Task:
        if not isinstance(task, Task):
            raise InvalidTaskError("task must be a Task instance")
        task.reopen()
        return task

    def cancel(self, task: Task) -> Task:
        if not isinstance(task, Task):
            raise InvalidTaskError("task must be a Task instance")
        task.cancel()
        return task

    def extract_from_note(self, note: Note) -> list[Task]:
        if not isinstance(note, Note):
            raise InvalidTaskError("note must be a Note instance")
        tasks: list[Task] = []
        for line in note.content.splitlines():
            stripped = line.strip()
            if stripped.startswith("- [ ] "):
                tasks.append(self.create(stripped[6:], note_path=note.path, tags=list(note.tags)))
            elif stripped.startswith("- [x] ") or stripped.startswith("- [X] "):
                task = self.create(stripped[6:], note_path=note.path, tags=list(note.tags))
                task.complete()
                tasks.append(task)
        return tasks

    def sync_note_tasks(self, note: Note) -> list[Task]:
        if not isinstance(note, Note):
            raise InvalidTaskError("note must be a Note instance")
        tasks = self.extract_from_note(note)
        note.frontmatter["tasks"] = [
            {
                "text": task.text.value,
                "state": task.state.value,
                "note_path": task.note_path.value if task.note_path is not None else None,
                "due_date": task.due_date.isoformat() if task.due_date is not None else None,
                "completed_at": task.completed_at.isoformat() if task.completed_at is not None else None,
                "tags": [tag.value for tag in task.tags],
            }
            for task in tasks
        ]
        note.frontmatter["task_count"] = len(tasks)
        note.frontmatter["open_task_count"] = sum(1 for task in tasks if task.state == TaskState.OPEN)
        note.frontmatter["done_task_count"] = sum(1 for task in tasks if task.state == TaskState.DONE)
        return tasks


@dataclass
class CanvasService:
    def add_note_node(self, canvas: Canvas, node_id: str, note: Note, x: int = 0, y: int = 0) -> CanvasNode:
        if not isinstance(canvas, Canvas):
            raise InvalidCanvasError("canvas must be a Canvas instance")
        if not isinstance(note, Note):
            raise InvalidCanvasError("note must be a Note instance")
        node = CanvasNode(id=node_id, kind=CanvasNodeType.NOTE, x=x, y=y, note_path=note.path)
        return canvas.add_node(node)

    def add_text_node(self, canvas: Canvas, node_id: str, text: str, x: int = 0, y: int = 0) -> CanvasNode:
        if not isinstance(canvas, Canvas):
            raise InvalidCanvasError("canvas must be a Canvas instance")
        node = CanvasNode(id=node_id, kind=CanvasNodeType.TEXT, x=x, y=y, text=text)
        return canvas.add_node(node)

    def link(self, canvas: Canvas, from_id: str, to_id: str, label: str | None = None) -> CanvasEdge:
        if not isinstance(canvas, Canvas):
            raise InvalidCanvasError("canvas must be a Canvas instance")
        return canvas.add_edge(CanvasEdge(from_id=from_id, to_id=to_id, label=label))

    def group(self, canvas: Canvas, group_id: str, node_ids: list[str]) -> CanvasNode:
        if not isinstance(canvas, Canvas):
            raise InvalidCanvasError("canvas must be a Canvas instance")
        return canvas.group_nodes(group_id, node_ids)

    def layout_horizontal(self, canvas: Canvas, *, start_x: int = 0, start_y: int = 0, gap: int = 320) -> Canvas:
        if not isinstance(canvas, Canvas):
            raise InvalidCanvasError("canvas must be a Canvas instance")
        canvas.layout_horizontal(start_x=start_x, start_y=start_y, gap=gap)
        return canvas

    def layout_vertical(self, canvas: Canvas, *, start_x: int = 0, start_y: int = 0, gap: int = 220) -> Canvas:
        if not isinstance(canvas, Canvas):
            raise InvalidCanvasError("canvas must be a Canvas instance")
        canvas.layout_vertical(start_x=start_x, start_y=start_y, gap=gap)
        return canvas

    def layout_grid(
        self,
        canvas: Canvas,
        *,
        columns: int = 2,
        start_x: int = 0,
        start_y: int = 0,
        gap_x: int = 320,
        gap_y: int = 220,
    ) -> Canvas:
        if not isinstance(canvas, Canvas):
            raise InvalidCanvasError("canvas must be a Canvas instance")
        canvas.layout_grid(columns=columns, start_x=start_x, start_y=start_y, gap_x=gap_x, gap_y=gap_y)
        return canvas

    def get_node(self, canvas: Canvas, node_id: str) -> CanvasNode:
        if not isinstance(canvas, Canvas):
            raise InvalidCanvasError("canvas must be a Canvas instance")
        return canvas.get_node(node_id)

    def get_edge(self, canvas: Canvas, from_id: str, to_id: str) -> CanvasEdge:
        if not isinstance(canvas, Canvas):
            raise InvalidCanvasError("canvas must be a Canvas instance")
        return canvas.get_edge(from_id, to_id)

    def remove_node(self, canvas: Canvas, node_id: str) -> CanvasNode:
        if not isinstance(canvas, Canvas):
            raise InvalidCanvasError("canvas must be a Canvas instance")
        return canvas.remove_node(node_id)

    def remove_edge(self, canvas: Canvas, from_id: str, to_id: str) -> CanvasEdge:
        if not isinstance(canvas, Canvas):
            raise InvalidCanvasError("canvas must be a Canvas instance")
        return canvas.remove_edge(from_id, to_id)

    def remove_node(self, canvas: Canvas, node_id: str) -> CanvasNode:
        if not isinstance(canvas, Canvas):
            raise InvalidCanvasError("canvas must be a Canvas instance")
        return canvas.remove_node(node_id)

    def remove_edge(self, canvas: Canvas, from_id: str, to_id: str) -> CanvasEdge:
        if not isinstance(canvas, Canvas):
            raise InvalidCanvasError("canvas must be a Canvas instance")
        return canvas.remove_edge(from_id, to_id)
