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

from __future__ import annotations

import re
from pathlib import Path

from sbo_create.handlers.download import DownloadHandler
from sbo_create.handlers.files import FileHandler
from sbo_create.handlers.validate import ValidateHandler
from sbo_create.models import PROG, UNSUPPORTED, AppContext, colorize, confirm


class BumpHandler:  # pylint: disable=too-few-public-methods
    """Handle version bumping of SlackBuild package files.

    Updates VERSION in .info and .SlackBuild, optionally replaces version
    strings in DOWNLOAD URLs, downloads sources and recomputes MD5SUMs.
    """

    def __init__(self, ctx: AppContext, files: FileHandler,
                 validate: ValidateHandler, download: DownloadHandler) -> None:
        self.ctx = ctx
        self.files = files
        self.validate = validate
        self.download = download

    def _bump_download_urls(self, content: str, old_version: str,
                            new_version: str) -> tuple[str, bool]:
        """Replace the old version string with the new one in DOWNLOAD URL lines.

        Iterates over DOWNLOAD and DOWNLOAD_x86_64 blocks. Warns about URLs that
        contain 'http' but no version string (manual update required).

        Args:
            content: Full .info file content as a string.
            old_version: The version string to replace.
            new_version: The replacement version string.

        Returns:
            A tuple of (updated_content, urls_changed) where urls_changed is True
            if at least one URL was modified.
        """
        lines: list[str] = []
        warn_files: list[str] = []
        urls_changed: bool = False
        in_download = False
        for line in content.splitlines(keepends=True):
            if re.match(r'DOWNLOAD', line):
                in_download = True

            if in_download:
                if old_version in line:
                    line = line.replace(old_version, new_version)
                    urls_changed = True
                elif 'http' in line:
                    warn_files.append(line.split('/')[-1].rstrip('"\n\\ '))

                if '"' in line and not line.rstrip().endswith('\\'):
                    in_download = False

            lines.append(line)

        if warn_files:
            warn: str = colorize('WARNING', 'yellow')
            print(f"  {'DOWNLOAD':<12} {warn}  {len(warn_files)} URLs have no version string — update manually")

        return ''.join(lines), urls_changed

    def _format_info_field(self, label: str, values: list[str]) -> str:
        """Format a .info field, using multi-line continuation for multiple values.

        Args:
            label: The field name (e.g. 'MD5SUM').
            values: List of value strings (checksums or URLs).

        Returns:
            A properly formatted field string, e.g. 'MD5SUM="abc123"' or a
            multi-line version with backslash continuation.
        """
        if len(values) == 1:
            return f'{label}="{values[0]}"'
        body: str = ' \\\n  '.join(values)
        return f'{label}="{body}"'

    def _bump_md5sums(self, content: str) -> tuple[str, bool]:
        """Download sources and recompute MD5SUMs, updating the .info content.

        Reads current download URLs from AppContext, downloads each source,
        computes its MD5 checksum and substitutes the new values into the content.

        Args:
            content: Current .info file content as a string.

        Returns:
            A tuple of (updated content, all_ok) where all_ok is False if any
            download failed (i.e. run_downloads returned an empty MD5 string).
        """
        self.files.read_info_file()

        groups: list[tuple[str, str, int, int]] = []
        all_links: list[str] = []
        for attr, label in [
            ('download_x86', 'MD5SUM'),
            ('download_x86_64', 'MD5SUM_x86_64'),
        ]:
            downloads: str = getattr(self.ctx.info, attr)
            if not downloads or downloads in UNSUPPORTED:
                continue
            links: list[str] = downloads.split()
            groups.append((attr, label, len(all_links), len(all_links) + len(links)))
            all_links.extend(links)

        if not all_links:
            return content, True

        all_md5s: list[str] = self.download.run_downloads(all_links, {}, verify=False)
        all_ok: bool = all(all_md5s)

        for _attr, label, start, end in groups:
            valid_md5s: list[str] = [m for m in all_md5s[start:end] if m]
            if valid_md5s:
                replacement: str = self._format_info_field(label, valid_md5s)
                content = re.sub(rf'{label}="[^"]*"', replacement, content)

        return content, all_ok

    def bump_version(self, new_version: str, with_sources: bool = False) -> None:  # pylint: disable=too-many-branches
        """Bump VERSION in .info and .SlackBuild files to the given new version.

        Updates VERSION="..." in the .info file, replaces the old version string
        in DOWNLOAD URLs, and updates the ${VERSION:-...} default in .SlackBuild.
        With --with-sources, also downloads new sources and recomputes MD5SUMs,
        then runs validation to confirm the bump is clean.

        Args:
            new_version: The new version string to write.
            with_sources: If True, download sources and recompute MD5SUMs after
                updating version and URLs.
        """
        info_file: Path = Path(self.ctx.current_folder, f'{self.ctx.prg_name}.info')
        slackbuild: Path = Path(self.ctx.current_folder, f'{self.ctx.prg_name}.SlackBuild')

        if not info_file.is_file() and not slackbuild.is_file():
            print(colorize(f"{PROG}: No .info or .SlackBuild files found.", 'red'))
            raise SystemExit(1)

        old_version: str = ''
        content: str = ''
        if info_file.is_file():
            content = info_file.read_text(encoding='utf-8')
            m = re.search(r'VERSION="([^"]*)"', content)
            if m:
                old_version = m.group(1)

        print(f"Bumping version {old_version} → {new_version}\n")

        urls_changed: bool = False
        if content:
            content = re.sub(r'VERSION="[^"]*"', f'VERSION="{new_version}"', content)
            print(f"  {'VERSION':<12} {colorize('OK', 'green')}")
            if old_version:
                content, urls_changed = self._bump_download_urls(content, old_version, new_version)
            info_file.write_text(content, encoding='utf-8')

        if slackbuild.is_file():
            sb_content: str = slackbuild.read_text(encoding='utf-8')
            sb_content = re.sub(r'\$\{VERSION:-[^}]+\}', f'${{VERSION:-{new_version}}}', sb_content)
            slackbuild.write_text(sb_content, encoding='utf-8')
            print(f"  {'SlackBuild':<12} {colorize('OK', 'green')}")

        if with_sources and content:
            if not confirm("Download sources and recompute checksums?"):
                raise SystemExit(0)
            content, all_ok = self._bump_md5sums(content)
            info_file.write_text(content, encoding='utf-8')
            if all_ok:
                print(f"  {'MD5SUM':<12} {colorize('OK', 'green')}")
                print(colorize(f"\nVersion bumped to '{new_version}'.", 'green'))
            else:
                print(f"  {'MD5SUM':<12} {colorize('FAILED', 'red')}")
                print(colorize(f"\nVersion bumped to '{new_version}', but some sources could not be downloaded.", 'yellow'))
                print(colorize("  MD5SUMs for failed downloads were not updated.", 'yellow'))
            raise SystemExit(0)

        print()
        if urls_changed:
            print(f"  {colorize('Note:', 'yellow')} DOWNLOAD URLs updated but MD5SUM not recomputed.")
            print(f"         Run '{colorize(f'sbc bump {new_version} --with-sources', 'cyan')}' to update checksums.")
            print()
        print(colorize(f"Version bumped to '{new_version}'.", 'green'))
        raise SystemExit(0)
