"""Journal service: user journal + agent observations, always separate files.

Section names ("Focus", "Mood", etc.) are canonical ASCII keys — they are
structural identifiers used by code, vault tooling, and PKM editors, the
same way HTML tag names or frontmatter field names stay in English. Every
piece of user-visible text (titles, descriptions, section hints) is loaded
through ``infra.i18n.t()`` so the content renders in the user's locale.

Section definitions, file naming, and timestamp format are loaded from the
``journal-template`` meta-skill's ``config/sections.yaml`` (Rule #12).
Hardcoded fallbacks are kept for backward compat when the skill isn't loaded.
"""

from __future__ import annotations

import logging
import re
from pathlib import Path
from typing import TYPE_CHECKING

from agntspace.infra.i18n import t
from agntspace.vault.reader import VaultDocument, VaultReader
from agntspace.vault.writer import VaultWriter

if TYPE_CHECKING:
    from agntspace.skills.loader import SkillLoader

log = logging.getLogger(__name__)

# ── Hardcoded fallbacks (used only when skill config is unavailable) ──

_FALLBACK_USER_SECTIONS: tuple[tuple[str, str], ...] = (
    ("Focus", "journal.user_hints.focus"),
    ("Mood", "journal.user_hints.mood"),
    ("Gratitude", "journal.user_hints.gratitude"),
    ("Thoughts", "journal.user_hints.thoughts"),
    ("Notes", "journal.user_hints.notes"),
    ("Questions", "journal.user_hints.questions"),
    ("Decisions", "journal.user_hints.decisions"),
    ("Learnings", "journal.user_hints.learnings"),
    ("Blockers", "journal.user_hints.blockers"),
    ("Reflections", "journal.user_hints.reflections"),
)

_FALLBACK_AGENT_SECTIONS: tuple[tuple[str, str], ...] = (
    ("Observations", "journal.agent_hints.observations"),
    ("Actions", "journal.agent_hints.actions"),
    ("Tool Activity", "journal.agent_hints.tool_activity"),
    ("Subagent Activity", "journal.agent_hints.subagent_activity"),
    ("System Health", "journal.agent_hints.system_health"),
    ("Knowledge Changes", "journal.agent_hints.knowledge_changes"),
    ("User Patterns", "journal.agent_hints.user_patterns"),
    ("Notes", "journal.agent_hints.notes"),
)

_FALLBACK_FILE_TYPES: dict[str, dict[str, str]] = {
    "journal": {"prefix": "user-journal_", "dir": "user"},
    "observations": {"prefix": "agent-observations_", "dir": "agnt"},
    "briefing": {"prefix": "morning-briefing_", "dir": "agnt"},
    "eod": {"prefix": "eod-report_", "dir": "agnt"},
    "reflection": {"prefix": "daily-reflection_", "dir": "agnt"},
    "weekly_reflection": {"prefix": "weekly-reflection_", "dir": "agnt"},
}

_FALLBACK_TIMESTAMP_FORMAT = "%H:%M"


def _load_sections_config(skill_loader: SkillLoader | None) -> dict | None:
    """Load sections.yaml from the journal-template meta-skill."""
    if not skill_loader:
        return None
    try:
        return skill_loader.load_skill_config_file("journal-template", "sections.yaml")
    except Exception:
        return None


def _parse_section_list(raw: list[dict] | None) -> tuple[tuple[str, str], ...] | None:
    """Parse a YAML section list into the (key, hint_i18n) tuple format."""
    if not raw or not isinstance(raw, list):
        return None
    result = []
    for entry in raw:
        if isinstance(entry, dict) and "key" in entry:
            result.append((entry["key"], entry.get("hint_i18n", "")))
    return tuple(result) if result else None


def _build_template(
    date: str,
    sections: tuple[tuple[str, str], ...],
    title_key: str,
    desc_key: str,
    author: str,
    extra_fm: dict[str, str] | None = None,
) -> str:
    """Build a journal template from section definitions."""
    title = t(title_key, date=date)
    description = t(desc_key)
    parts = [
        "---",
        f'title: "{title}"',
        f'date: "{date}"',
        f"author: {author}",
    ]
    if extra_fm:
        for k, v in extra_fm.items():
            parts.append(f"{k}: {v}")
    parts.append(f'description: "{description}"')
    parts.append("---")
    parts.append("")
    for header, hint_key in sections:
        parts.append(f"## {header}")
        if hint_key:
            parts.append(f"*{t(hint_key)}*")
        parts.append("")
    return "\n".join(parts)


class JournalService:
    """Manages daily journal entries in vault/journal/.

    Section structure, file types, and timestamp format are loaded from the
    ``journal-template`` meta-skill's ``config/sections.yaml`` at init time,
    with hardcoded fallbacks for backward compat.
    """

    # Directory constants
    _USER_DIR = "journal/user"
    _AGNT_DIR = "journal/agnt"

    def __init__(
        self,
        vault_reader: VaultReader,
        vault_writer: VaultWriter,
        skill_loader: SkillLoader | None = None,
    ) -> None:
        self._reader = vault_reader
        self._writer = vault_writer
        self._skill_loader = skill_loader

        # Load config from skill YAML, fall back to hardcoded defaults
        cfg = _load_sections_config(skill_loader)

        self._user_sections = (
            _parse_section_list(cfg.get("user_sections")) if cfg else None
        ) or _FALLBACK_USER_SECTIONS

        self._agent_sections = (
            _parse_section_list(cfg.get("agent_sections")) if cfg else None
        ) or _FALLBACK_AGENT_SECTIONS

        self._file_types: dict[str, dict[str, str]] = (
            cfg.get("file_types") if cfg and isinstance(cfg.get("file_types"), dict) else None
        ) or _FALLBACK_FILE_TYPES

        self._timestamp_format: str = (
            cfg.get("timestamp_format") if cfg else None
        ) or _FALLBACK_TIMESTAMP_FORMAT

        if cfg:
            log.debug("Journal sections loaded from journal-template skill config")
        else:
            log.debug("Journal sections using hardcoded fallbacks")

    # ---- Public accessors for config-driven data ----

    def get_user_section_keys(self) -> list[str]:
        """Return the list of valid user journal section keys."""
        return [key for key, _ in self._user_sections]

    def get_agent_section_keys(self) -> list[str]:
        """Return the list of valid agent observation section keys."""
        return [key for key, _ in self._agent_sections]

    def get_valid_file_types(self) -> set[str]:
        """Return the set of recognized file type keys."""
        return set(self._file_types.keys())

    # ---- Path helpers ----

    def _user_path(self, date: str | None = None) -> str:
        if not date:
            from agntspace.infra.timezone import local_today
            date = local_today()
        ft = self._file_types.get("journal", _FALLBACK_FILE_TYPES["journal"])
        return f"{self._USER_DIR}/{ft['prefix']}{date}.md"

    def _agent_path(self, date: str | None = None) -> str:
        if not date:
            from agntspace.infra.timezone import local_today
            date = local_today()
        ft = self._file_types.get("observations", _FALLBACK_FILE_TYPES["observations"])
        return f"{self._AGNT_DIR}/{ft['prefix']}{date}.md"

    def today_path(self) -> str:
        """Return the relative vault path for today's user journal."""
        return self._user_path()

    # ---- Read ----

    def get_today(self) -> VaultDocument:
        """Get or create today's user journal entry."""
        path = self._user_path()
        if not self._reader.exists(path):
            self._create_user_entry(path)
        return self._reader.read(path)

    def get_entry(self, date: str) -> VaultDocument | None:
        """Get a user journal entry by date string (YYYY-MM-DD)."""
        path = self._user_path(date)
        if not self._reader.exists(path):
            return None
        return self._reader.read(path)

    def get_agent_observations(self, date: str | None = None) -> VaultDocument | None:
        """Get agent observations for a date."""
        path = self._agent_path(date)
        if not self._reader.exists(path):
            return None
        return self._reader.read(path)

    def list_entries(self, limit: int = 30) -> list[dict]:
        """List recent user journal entries (newest first)."""
        journal_prefix = self._file_types.get("journal", _FALLBACK_FILE_TYPES["journal"])["prefix"]
        user_files = [
            f for f in self._reader.list_files(self._USER_DIR, "*.md")
            if f.name.startswith(journal_prefix)
        ]
        user_files.sort(key=lambda f: f.name, reverse=True)
        return [
            {"date": f.stem.replace(journal_prefix.rstrip("_"), "").lstrip("_"), "path": f"{self._USER_DIR}/{f.name}"}
            for f in user_files[:limit]
        ]

    def list_dates(self, limit: int = 30) -> list[dict]:
        """List recent journal dates with file types and status flags (newest first)."""
        date_map: dict[str, dict] = {}
        # Build prefix→file_type map from config
        _PREFIX_MAP = {
            ft_info["prefix"]: ft_key
            for ft_key, ft_info in self._file_types.items()
        }

        all_files = []
        for d in (self._USER_DIR, self._AGNT_DIR):
            all_files.extend(self._reader.list_files(d, "*.md"))

        for f in all_files:
            if f.suffix != ".md":
                continue
            name = f.name
            for prefix, file_type in _PREFIX_MAP.items():
                if name.startswith(prefix):
                    date = name.replace(prefix, "").replace(".md", "")
                    date_map.setdefault(date, {"date": date, "types": [], "flags": []})
                    if file_type not in date_map[date]["types"]:
                        date_map[date]["types"].append(file_type)
                    break

        # Enrich with status flags (lightweight — only reads metadata/sections, not full content)
        for date, entry in date_map.items():
            flags = entry["flags"]
            # Check reflection status
            if "reflection" in entry["types"]:
                refl_prefix = self._file_types.get("reflection", _FALLBACK_FILE_TYPES["reflection"])["prefix"]
                refl_path = f"{self._AGNT_DIR}/{refl_prefix}{date}.md"
                if self._reader.exists(refl_path):
                    doc = self._reader.read(refl_path)
                    status = doc.metadata.get("status", "")
                    if status == "pending":
                        flags.append("pending_reflection")
                    elif status == "approved":
                        flags.append("approved_reflection")
            # Check for blockers and questions in user journal
            if "journal" in entry["types"]:
                blockers = self.get_section("Blockers", date)
                if blockers:
                    flags.append("has_blockers")
                questions = self.get_section("Questions", date)
                if questions:
                    flags.append("has_questions")

        result = sorted(date_map.values(), key=lambda x: x["date"], reverse=True)
        return result[:limit]

    def _resolve_agnt_path(self, filename: str) -> str:
        """Resolve an agent journal file."""
        return f"{self._AGNT_DIR}/{filename}"

    def get_file_for_date(self, date: str, file_type: str) -> VaultDocument | None:
        """Get a specific journal file type for a date."""
        ft_info = self._file_types.get(file_type)
        if not ft_info:
            return None
        prefix = ft_info["prefix"]
        ft_dir = ft_info.get("dir", "agnt")
        if ft_dir == "user":
            path = f"{self._USER_DIR}/{prefix}{date}.md"
        else:
            path = f"{self._AGNT_DIR}/{prefix}{date}.md"
        if not self._reader.exists(path):
            return None
        return self._reader.read(path)

    def get_section(self, section: str, date: str | None = None) -> str | None:
        """Read a specific section from the user journal."""
        path = self._user_path(date)
        if not self._reader.exists(path):
            return None
        doc = self._reader.read(path)
        # Extract section content between ## headers
        pattern = rf"## {re.escape(section)}\b.*?\n(.*?)(?=\n## |\Z)"
        match = re.search(pattern, doc.raw, re.DOTALL)
        if match:
            content = match.group(1).strip()
            # Strip HTML comments and italic hint lines
            content = re.sub(r"<!--.*?-->", "", content, flags=re.DOTALL)
            content = re.sub(r"^\*[^*]+\*$", "", content, flags=re.MULTILINE)
            content = content.strip()
            return content if content else None
        return None

    # ---- User writes (to user journal — user-only, agent never writes here) ----

    def append_to_user(self, section: str, text: str) -> None:
        """Append a timestamped entry to any user journal section."""
        self._append_to_user_section(section, f"- {self._timestamp()} — {text}")

    # Convenience methods kept for backward compat — delegate to generic append.
    def set_focus(self, text: str) -> None:
        self.append_to_user("Focus", text)

    def append_mood(self, text: str) -> None:
        self.append_to_user("Mood", text)

    def append_gratitude(self, text: str) -> None:
        self.append_to_user("Gratitude", text)

    def append_thought(self, text: str) -> None:
        self.append_to_user("Thoughts", text)

    def append_note(self, text: str) -> None:
        self.append_to_user("Notes", text)

    def append_question(self, text: str) -> None:
        self.append_to_user("Questions", text)

    def append_decision(self, text: str) -> None:
        self.append_to_user("Decisions", text)

    def append_learning(self, text: str) -> None:
        self.append_to_user("Learnings", text)

    def append_blocker(self, text: str) -> None:
        self.append_to_user("Blockers", text)

    def append_reflection(self, text: str) -> None:
        self.append_to_user("Reflections", text)

    # ---- Agent writes (to agent observations file) ----

    def append_observation(self, text: str) -> None:
        """Append an agent observation to today's agent file."""
        self._append_to_agent_section(
            "Observations",
            f"- {self._timestamp()} — {text}",
        )

    def append_action(self, text: str) -> None:
        """Append an agent action to today's agent file."""
        self._append_to_agent_section(
            "Actions",
            f"- {self._timestamp()} — {text}",
        )

    def append_agent_note(self, text: str) -> None:
        """Append an agent note to today's agent file."""
        self._append_to_agent_section(
            "Notes",
            f"- {self._timestamp()} — {text}",
        )

    # ---- Internal ----

    def _append_to_user_section(self, section: str, line: str) -> None:
        """Append a line to a section of today's user journal."""
        path = self._user_path()
        if not self._reader.exists(path):
            self._create_user_entry(path)
        self._append_to_section(path, section, line, trust_reason="journal_write")

    def _append_to_agent_section(self, section: str, line: str) -> None:
        """Append a line to a section of today's agent observations."""
        path = self._agent_path()
        if not self._reader.exists(path):
            self._create_agent_entry(path)
        self._append_to_section(path, section, line, trust_reason="agent_journal_write")

    def _append_to_section(self, path: str, section: str, line: str, *, trust_reason: str = "journal_write") -> None:
        """Append a line to a specific section of a file.

        Dedup: extracts the text after the timestamp+author tag and skips
        if that same text already appears in the section (prevents
        duplicate entries from restarts / repeated tool calls).
        """
        doc = self._reader.read(path)
        content = doc.raw

        # Find the section and append before the next section or end
        pattern = rf"(## {re.escape(section)}\b.*?)(\n## |\Z)"
        match = re.search(pattern, content, re.DOTALL)
        if match:
            section_text = match.group(1)

            # Dedup: extract the meaningful text (after "HH:MM [author] — " or "HH:MM — ")
            dedup_text = re.sub(r"^- \d{2}:\d{2}\s*(\[\w+\]\s*)?—\s*", "", line).strip()
            if dedup_text and dedup_text in section_text:
                return  # already present — skip

            section_end = match.end(1)
            new_content = content[:section_end] + "\n" + line + content[section_end:]
            self._writer.write(path, new_content, trust_reason=trust_reason)
        else:
            self._writer.write(path, content + f"\n## {section}\n{line}\n", trust_reason=trust_reason)

    def _create_user_entry(self, path: str) -> None:
        journal_prefix = self._file_types.get("journal", _FALLBACK_FILE_TYPES["journal"])["prefix"]
        date = Path(path).stem.replace(journal_prefix.rstrip("_"), "").lstrip("_")
        content = _build_template(
            date, self._user_sections,
            "journal.user_title", "journal.user_description",
            "user",
        )
        self._writer.write(path, content, trust_reason="journal_create")

    def _create_agent_entry(self, path: str) -> None:
        date = path.split("_")[-1].replace(".md", "")
        content = _build_template(
            date, self._agent_sections,
            "journal.agent_title", "journal.agent_description",
            "agent",
            extra_fm={"type": "agent_observations", "agent": "main"},
        )
        self._writer.write(path, content, trust_reason="agent_journal_create")

    def _timestamp(self) -> str:
        from agntspace.infra.timezone import now_local
        return now_local().strftime(self._timestamp_format)

    @staticmethod
    def is_thinking_out_loud(text: str, vault_dir: Path | None = None) -> bool:
        """Detect if a user message sounds like thinking out loud.

        Phrase markers are loaded from the ``memory-consolidate`` skill's
        multilingual ``thinking-out-loud.yaml`` so the classifier works in
        any user language without hardcoded English regex.
        """
        from agntspace.infra.language_patterns import load_keywords

        if not text:
            return False

        # Normalize: lowercase, expand common contractions, strip punctuation.
        lower = text.lower().strip()
        lower = (
            lower.replace("'m ", " am ")
            .replace("'ve ", " have ")
            .replace("'re ", " are ")
            .replace("'s ", " is ")
        )
        # Collapse "i am" / "i have" / "i've" etc. down to "i " so YAML
        # phrases like "i thinking" match "i am thinking" and "i'm thinking".
        collapsed = re.sub(r"\bi\s+(am|have|had|was|will|would)\s+", "i ", lower)

        phrases = load_keywords(
            "core/memory-consolidate/config/thinking-out-loud.yaml",
            vault_dir=vault_dir,
        )
        if not phrases:
            return False
        return any(collapsed.startswith(p) or lower.startswith(p) for p in phrases)
