from __future__ import annotations

import itertools
import os
import re
import tempfile
from dataclasses import dataclass
from pathlib import Path
from string import Template

from utils.cache.files import read_text
from utils.env.parser import parse_static_env


@dataclass(frozen=True)
class CoreDNSCorefileRenderer:
    """
    Render compose/coredns/Corefile from compose/coredns/Corefile.tmpl.

    What this does:
      - Reads variables from env file (default: .env, generated from default.env)
      - Substitutes ``${VAR}`` placeholders into the Corefile template
      - Writes the output atomically (tmp -> rename)
      - Optionally prints a preview of the first N lines

    Substitution runs in-process rather than through envsubst: the act runner and
    the nested deploy containers do not all ship gettext, and a missing binary
    turned a two-variable substitution into a failed deploy.

    Hard guarantees:
      - Fails if template/env files are missing
      - Fails if output path exists and is a directory
      - Fails if output directory cannot be created or is not writable
      - Fails if rendered file is empty
    """

    repo_root: Path
    env_filename: str = ".env"
    template_relpath: str = "compose/coredns/Corefile.tmpl"
    output_relpath: str = "compose/coredns/Corefile"

    def _log(self, msg: str) -> None:
        print(f"[coredns-corefile] {msg}")

    def _paths(self) -> tuple[Path, Path, Path]:
        env_file = self.repo_root / self.env_filename
        tmpl_file = self.repo_root / self.template_relpath
        out_file = self.repo_root / self.output_relpath
        return env_file, tmpl_file, out_file

    def _require_file(self, path: Path, *, label: str) -> None:
        if not path.exists():
            raise RuntimeError(f"{label} not found: {path}")
        if not path.is_file():
            raise RuntimeError(f"{label} is not a file: {path}")

    def _ensure_output_parent(self, out_file: Path) -> None:
        parent = out_file.parent

        if parent.exists() and not parent.is_dir():
            raise RuntimeError(f"Output parent is not a directory: {parent}")

        if not parent.exists():
            self._log(f"Creating output directory: {parent}")
            try:
                parent.mkdir(parents=True, exist_ok=True)
            except OSError as exc:
                raise RuntimeError(
                    f"Failed to create output directory: {parent}"
                ) from exc

        if not os.access(parent, os.W_OK):
            raise RuntimeError(f"Output directory is not writable: {parent}")

    def _require_output_target(self, out_file: Path) -> None:
        if out_file.exists() and out_file.is_dir():
            raise RuntimeError(f"Output path is a directory: {out_file}")

    def _load_env_file(self, env_file: Path) -> dict[str, str]:
        self._log(f"Loading env file: {env_file}")
        env = dict(os.environ)
        parsed = parse_static_env(env_file)
        env.update(parsed)
        self._log(f"Loaded {len(parsed)} variables from env file")
        return env

    def _preview(self, path: Path, *, max_lines: int) -> None:
        if not path.exists() or not path.is_file():
            self._log(f"Preview skipped: {path}")
            return

        self._log(f"Preview (first {max_lines} lines): {path}")
        with path.open("r", encoding="utf-8", errors="replace") as f:
            for line in itertools.islice(f, max_lines):
                self._log(f"  {line.rstrip()}")

    def render(self, *, show_preview: bool = True, preview_lines: int = 25) -> Path:
        env_file, tmpl_file, out_file = self._paths()

        self._log(f"Repo root : {self.repo_root}")
        self._log(f"Template  : {tmpl_file}")
        self._log(f"Output    : {out_file}")

        self._require_file(env_file, label="env file")
        self._require_file(tmpl_file, label="template file")
        self._require_output_target(out_file)
        self._ensure_output_parent(out_file)

        env = self._load_env_file(env_file)

        fd, tmp_name = tempfile.mkstemp(
            dir=out_file.parent, prefix=out_file.name + ".", suffix=".tmp"
        )
        tmp_file = Path(tmp_name)

        self._log("Rendering Corefile (atomic write)")
        try:
            rendered = Template(read_text(tmpl_file)).safe_substitute(
                env, DOMAIN_RE=re.escape(env["INFINITO_DOMAIN"])
            )
            with os.fdopen(fd, "w", encoding="utf-8") as fout:
                fout.write(rendered)

            size = tmp_file.stat().st_size
            if size == 0:
                raise RuntimeError(f"Rendered Corefile is empty: {tmp_file}")

            tmp_file.chmod(0o644)
            tmp_file.replace(out_file)
        except OSError as exc:
            raise RuntimeError(
                f"Failed to write temporary Corefile: {tmp_file}"
            ) from exc
        finally:
            tmp_file.unlink(missing_ok=True)

        self._log(f"Rendered successfully ({size} bytes)")

        if show_preview:
            self._preview(out_file, max_lines=int(preview_lines))

        return out_file
