"""Safe file writer for NIKAME generated output.

Handles directory creation, dry-run previews, .gitignore tracking,
and idempotent writes. All file output in NIKAME goes through this module.
"""

from __future__ import annotations

import json
import secrets
from pathlib import Path
from typing import Any

import yaml

from nikame.exceptions import NikameGenerationError
from nikame.utils.logger import console, get_logger

_log = get_logger("file_writer")


class FileWriter:
    """Writes generated files to an output directory.

    Supports dry-run mode (preview without writing), tracks written
    files for summary output, and auto-generates .env files with
    secure random secrets.

    When buffered=True, files are stored in memory until flush() is called.
    This allows the Rules Engine to validate and fix content before writing.

    Args:
        output_dir: Root directory for generated output.
        dry_run: If True, print diffs instead of writing.
        buffered: If True, hold files in memory until flush().
    """

    def __init__(self, output_dir: Path, *, dry_run: bool = False, buffered: bool = False) -> None:
        self.output_dir = output_dir
        self.dry_run = dry_run
        self.buffered = buffered
        self._written_files: list[Path] = []
        self._buffer: dict[str, str] = {}
        self._gitignore_entries: list[str] = [
            ".env.generated",
            ".nikame/",
        ]

    @property
    def buffer(self) -> dict[str, str]:
        """Return the in-memory file buffer (for Rules Engine access)."""
        return self._buffer

    @buffer.setter
    def buffer(self, value: dict[str, str]) -> None:
        self._buffer = value

    @property
    def written_files(self) -> list[Path]:
        """Return list of all files written during this session."""
        return list(self._written_files)

    def write_file(self, relative_path: str, content: str) -> Path:
        """Write content to a file within the output directory.

        In buffered mode, content is stored in memory.
        In normal mode, content is written directly to disk.

        Args:
            relative_path: Path relative to output_dir.
            content: File content to write.

        Returns:
            Absolute path to the (eventual) file.

        Raises:
            NikameGenerationError: If file write fails.
        """
        target = self.output_dir / relative_path

        if self.dry_run:
            console.print(f"  [path]{relative_path}[/path]", style="dim")
            return target

        if self.buffered:
            self._buffer[relative_path] = content
            _log.debug("Buffered: %s", relative_path)
            return target

        try:
            target.parent.mkdir(parents=True, exist_ok=True)
            target.write_text(content, encoding="utf-8")
            self._written_files.append(target)
            _log.debug("Wrote: %s", target)
            return target
        except OSError as exc:
            raise NikameGenerationError(
                f"Failed to write file '{relative_path}': {exc}"
            ) from exc

    def flush(self) -> None:
        """Write all buffered files to disk.

        Called after the Rules Engine has validated and fixed the buffer.
        """
        if not self.buffered:
            return

        for relative_path, content in self._buffer.items():
            target = self.output_dir / relative_path
            try:
                target.parent.mkdir(parents=True, exist_ok=True)
                target.write_text(content, encoding="utf-8")
                self._written_files.append(target)
                _log.debug("Flushed: %s", target)
            except OSError as exc:
                raise NikameGenerationError(
                    f"Failed to flush file '{relative_path}': {exc}"
                ) from exc

        self._buffer.clear()

    def write_yaml(self, relative_path: str, data: dict[str, Any]) -> Path:
        """Serialize a dict to YAML and write it.

        Args:
            relative_path: Path relative to output_dir.
            data: Dict to serialize as YAML.

        Returns:
            Absolute path to the written file.
        """
        content = yaml.dump(
            data,
            default_flow_style=False,
            sort_keys=False,
            allow_unicode=True,
        )
        header = "# Auto-generated by NIKAME — DO NOT EDIT\n"
        return self.write_file(relative_path, header + content)

    def write_env_example(self, env_vars: dict[str, str]) -> Path:
        """Write .env.example with placeholder values.

        Args:
            env_vars: Dict of env var name → description or placeholder.

        Returns:
            Path to .env.example.
        """
        lines = ["# NIKAME — Environment Variables", "# Copy to .env and fill in values", ""]
        for key, description in sorted(env_vars.items()):
            lines.append(f"# {description}")
            lines.append(f"{key}=")
            lines.append("")
        return self.write_file(".env.example", "\n".join(lines))

    def write_env_generated(self, env_vars: dict[str, str]) -> Path:
        """Write .env.generated with auto-generated random secrets.

        This file is gitignored. Passwords get random values UNLESS
        a non-empty value is already provided; other vars get their
        default values.

        Args:
            env_vars: Dict of env var name → default or empty.

        Returns:
            Path to .env.generated.
        """
        lines = [
            "# NIKAME — Auto-generated secrets",
            "# THIS FILE IS GITIGNORED — do not commit",
            "",
        ]
        for key, default in sorted(env_vars.items()):
            if ("PASSWORD" in key or "SECRET" in key or "TOKEN" in key) and not default:
                value = secrets.token_urlsafe(32)
            else:
                value = default
            lines.append(f"{key}={value}")
        return self.write_file(".env.generated", "\n".join(lines))

    def write_gitignore(self, extra_entries: list[str] | None = None) -> Path:
        """Write .gitignore with NIKAME defaults.

        Args:
            extra_entries: Additional patterns to ignore.

        Returns:
            Path to .gitignore.
        """
        entries = list(self._gitignore_entries)
        if extra_entries:
            entries.extend(extra_entries)
        content = "\n".join(entries) + "\n"
        return self.write_file(".gitignore", content)

    def write_blueprint(self, blueprint_data: dict[str, Any]) -> Path:
        """Write .nikame/blueprint.json for idempotency tracking.

        Args:
            blueprint_data: Resolved blueprint graph data.

        Returns:
            Path to blueprint.json.
        """

        content = json.dumps(blueprint_data, indent=2, sort_keys=True)
        header = "// Auto-generated by NIKAME — DO NOT EDIT\n"
        return self.write_file(".nikame/blueprint.json", header + content)

    def print_summary(self) -> None:
        """Print summary of all written files to console."""
        if self.dry_run:
            console.print("\n[warning]DRY RUN — no files written[/warning]")
            return

        console.print(
            f"\n[success]✓ {len(self._written_files)} files generated[/success]"
        )
        for fp in self._written_files:
            rel = fp.relative_to(self.output_dir)
            console.print(f"  [path]{rel}[/path]")
