#!/usr/bin/python3
# -*- coding: utf-8 -*-

from __future__ import annotations

import re
from pathlib import Path

from sbo_create.models import AppContext, _Color


class TemplateLoader:
    """Load and render SlackBuild templates from the user's template directory.

    Substitutes placeholders in each template with values from AppContext
    before returning the rendered content as a string.
    """

    _BANNER_BG: str = "\x1b[48;5;4m"
    _BANNER_FG: str = "\x1b[38;5;7m"

    def __init__(self, ctx: AppContext) -> None:
        self.ctx = ctx

    def _read(self, filename: str) -> str:
        """Read a raw template file from the user's template directory.

        Args:
            filename: The template filename (e.g. 'autotools-template.SlackBuild').

        Returns:
            File contents as a UTF-8 string.

        Raises:
            FileNotFoundError: If the template file does not exist.
        """
        path: Path = self.ctx.templates_path / filename
        if not path.is_file():
            raise FileNotFoundError(
                f"Template '{filename}' not found. Run 'sbo-create -u' to update templates.")
        return path.read_text(encoding='utf-8')

    @staticmethod
    def _strip_body_comments(content: str) -> str:
        """Remove comment lines from the SlackBuild body, keeping the license header.

        The license header ends at the line containing 'ADVISED OF THE POSSIBILITY'.
        After the header, lines starting with '#' are dropped, consecutive blank
        lines are collapsed to one, and inline comments (text after ' # ') are
        stripped from non-comment lines.

        Args:
            content: Full SlackBuild file content as a string.

        Returns:
            The content with body comments removed.
        """
        lines: list[str] = content.splitlines(keepends=True)
        header_end: int = 0
        for i, line in enumerate(lines):
            if 'ADVISED OF THE POSSIBILITY' in line:
                header_end = i + 1
                break
        result: list[str] = lines[:header_end]
        prev_blank: bool = False
        for line in lines[header_end:]:
            if line.strip().startswith('#'):
                continue
            eol: str = '\n' if line.endswith('\n') else ''
            line = re.sub(r'\s+#\s.*', '', line.rstrip('\n')) + eol
            is_blank: bool = not line.strip()
            if is_blank and prev_blank:
                continue
            result.append(line)
            prev_blank = is_blank
        return ''.join(result)

    def slackbuild(self, template_type: str) -> str:
        """Return a rendered SlackBuild script with context placeholders substituted.

        Replaces 'appname', '<year>', '<you>', '<where you live>' and the default
        VERSION string with values from AppContext. Optionally strips body comments
        if ctx.strip_comments is True.

        Args:
            template_type: Template name without suffix (e.g. 'autotools', 'cmake').

        Returns:
            Rendered SlackBuild script as a string.
        """
        content: str = self._read(f'{template_type}-template.SlackBuild')
        version: str = self.ctx.info.version or '1.0.0'
        content = re.sub(r'\bappname\b', self.ctx.prg_name, content)
        content = content.replace('<year>', str(self.ctx.year))
        content = content.replace('<you>', self.ctx.maintainer.name)
        content = content.replace('<where you live>', self.ctx.maintainer.where_you_live)
        content = re.sub(r'\$\{VERSION:-[^}]+\}', f'${{VERSION:-{version}}}', content)
        if self.ctx.strip_comments:
            content = self._strip_body_comments(content)
        return content

    def info(self) -> str:
        """Return a rendered .info template with context placeholders substituted.

        Fills in PRGNAM, VERSION, MAINTAINER and EMAIL from context, and clears
        HOMEPAGE, DOWNLOAD, MD5SUM and REQUIRES to empty strings. Swaps x86_64
        field names for ARM64 variants if arm_support is set.

        Returns:
            Rendered .info file content as a string.
        """
        content: str = self._read('template.info')
        content = re.sub(r'PRGNAM="[^"]*"', f'PRGNAM="{self.ctx.prg_name}"', content)
        content = re.sub(r'VERSION="[^"]*"', f'VERSION="{self.ctx.info.version}"', content)
        content = re.sub(r'HOMEPAGE="[^"]*"', 'HOMEPAGE=""', content)
        content = re.sub(r'DOWNLOAD="[^"]*"', 'DOWNLOAD=""', content)
        content = re.sub(r'MD5SUM="[^"]*"', 'MD5SUM=""', content)
        content = re.sub(r'DOWNLOAD_x86_64="[^"]*"', 'DOWNLOAD_x86_64=""', content)
        content = re.sub(r'MD5SUM_x86_64="[^"]*"', 'MD5SUM_x86_64=""', content)
        content = re.sub(r'REQUIRES="[^"]*"', 'REQUIRES=""', content)
        content = re.sub(r'MAINTAINER="[^"]*"', f'MAINTAINER="{self.ctx.maintainer.name}"', content)
        content = re.sub(r'EMAIL="[^"]*"', f'EMAIL="{self.ctx.maintainer.email}"', content)
        if self.ctx.info.arm_support:
            content = content.replace('DOWNLOAD_x86_64', 'DOWNLOAD_ARM64')
            content = content.replace('MD5SUM_x86_64', 'MD5SUM_ARM64')
        return content

    def slack_desc(self) -> str:
        """Return a rendered slack-desc template with the package name substituted.

        Replaces the 'appname' placeholder and adjusts the handy-ruler indent
        to match the length of the package name.

        Returns:
            Rendered slack-desc content as a string.
        """
        name: str = self.ctx.prg_name or 'None'
        content: str = self._read('slack-desc')
        content = re.sub(r'^ {7}\|', f'{" " * len(name)}|', content, flags=re.MULTILINE)
        content = content.replace('appname', name)
        return content

    def doinst(self) -> str:
        """Return the doinst.sh template with an informational color banner prepended.

        The banner instructs the user to copy what they need and press 'q' to
        exit the pager. The banner is suppressed when color is disabled.

        Returns:
            doinst.sh content wrapped with header and footer banners.
        """
        cl: str = f'{self._BANNER_BG}{self._BANNER_FG}' if _Color.enabled else ''
        content: str = self._read('doinst.sh')
        header: str = (
            f'\n{cl}################################################################################\n'
            f'{cl}#                     COPY WHAT YOU WANT EXIT AND PASTE                        #\n'
            f'{cl}#                          PRESS "q or Q" TO EXIT                              #\n'
            f'{cl}################################################################################\n\n'
        )
        footer: str = f'\n{cl}################################################################################\n'
        return header + content + footer

    def douninst(self) -> str:
        """Return the douninst.sh template with an informational color banner prepended.

        The banner instructs the user to press 'q' to exit the pager.
        The banner is suppressed when color is disabled.

        Returns:
            douninst.sh content wrapped with a header banner.
        """
        cl: str = f'{self._BANNER_BG}{self._BANNER_FG}' if _Color.enabled else ''
        content: str = self._read('douninst.sh')
        header: str = (
            f'\n{cl}################################################################################\n'
            f'{cl}#                          PRESS "q or Q" TO EXIT                              #\n'
            f'{cl}################################################################################\n\n'
        )
        return header + content
