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

from __future__ import annotations

import argparse
import sys
from typing import Any, NoReturn

from sbo_create.models import PROG, colorize

# Checked before the parser runs so --help reflects --no-color even
# when the flag appears before or after the subcommand.
_COLOR_HELP: bool = '-N' not in sys.argv and '--no-color' not in sys.argv


class _ColorHelpFormatter(argparse.HelpFormatter):
    """Cargo-style colored help formatter.

    Colors section headings (yellow), flag/command names (green for
    options, cyan for positionals). Color is applied in _format_action
    *after* argparse has computed column widths from the plain text, so
    ANSI escape codes never inflate the measured string length.
    Falls back to plain text when --no-color / -N is set.
    """

    def start_section(self, heading: str | None) -> None:
        if heading and _COLOR_HELP:
            heading = colorize(heading, 'yellow')
        super().start_section(heading)

    def _format_action(self, action: argparse.Action) -> str:
        result: str = super()._format_action(action)
        if not _COLOR_HELP:
            return result
        # Retrieve the plain invocation text (already used for width
        # calculation above) and replace its first occurrence with the
        # colored version so the description column is unaffected.
        plain: str = super()._format_action_invocation(action)
        color: str = 'green' if action.option_strings else 'cyan'
        return result.replace(plain, colorize(plain, color), 1)


class _SBoCreateParser(argparse.ArgumentParser):
    """Custom argument parser with a cleaner unknown-command error message.

    Replaces argparse's default 'invalid choice' error with a user-friendly
    message that names the unknown command and points to --help.
    """

    def error(self, message: str) -> NoReturn:
        """Override the default error to improve unknown-command messages.

        Args:
            message: The error message produced by argparse.

        Raises:
            SystemExit: Always exits with code 2.
        """
        if 'invalid choice' in message and 'argument -' not in message:
            cmd: str = message.split("'")[1] if "'" in message else message
            self.exit(2, f"{PROG}: Unknown command '{cmd}'.\n"
                      f"Use '{PROG} --help' for available commands.\n")
        super().error(message)


def parse_args(version: str) -> argparse.Namespace:  # pylint: disable=too-many-statements
    """Build the argument parser and return the parsed CLI arguments.

    Args:
        version: Application version string injected into --version output.

    Returns:
        Parsed argument namespace with all subcommand flags populated.
    """
    # Hidden parent: forwards --no-color to subcommands without overriding
    # the value already set by the main parser when used before the subcommand.
    no_color_parent = _SBoCreateParser(add_help=False)
    no_color_parent.add_argument('-N', '--no-color', action='store_true',
                                 default=argparse.SUPPRESS, help=argparse.SUPPRESS)

    # Common kwargs shared by every sub.add_parser() call.
    _kw: dict[str, Any] = {
        'parents': [no_color_parent],
        'formatter_class': _ColorHelpFormatter,
    }

    parser = _SBoCreateParser(
        prog='sbo-create', add_help=False,
        formatter_class=_ColorHelpFormatter,
        epilog='For command-specific help: sbo-create <COMMAND> --help or man sbo-create.')
    options = parser.add_argument_group('Options')
    options.add_argument('-N', '--no-color', action='store_true',
                         help='Disable colored output.')
    options.add_argument('-v', '--version', action='version',
                         version=f'Version: {version}',
                         help='Show version and exit.')
    options.add_argument('-h', '--help', action='help',
                         help='Show this help message and exit.')

    sub = parser.add_subparsers(dest='command', metavar='COMMAND')

    # create
    sp = sub.add_parser('create', **_kw,
                        help='Create SlackBuild files in a new <prgnam> subdirectory.')
    sp.set_defaults(command='create')
    sp.add_argument('prgnam_pos', metavar='NAME', nargs='?', default=None,
                    help='Package name. Shorthand for -n; -n takes precedence if both given.')
    sp.add_argument('-n', '--prgnam', metavar='NAME',
                    help='Set the name of the SlackBuild (overrides positional NAME).')
    sp.add_argument('-r', '--prg-version', metavar='VERSION',
                    help='Set the version of the SlackBuild.')
    sp.add_argument('-t', '--template', metavar='TEMPLATE',
                    help='Set the SlackBuild template:'
                         ' {autotools, cmake, perl, python, rubygem, haskell, meson}.')
    sp.add_argument('-a', '--arm64', action='store_true',
                    help='Enable ARM64 architecture support.')
    sp.add_argument('-o', '--output', metavar='DIR',
                    help='Parent directory in which the <prgnam> subdirectory is created.')
    sp.add_argument('-D', '--dry-run', action='store_true',
                    help='Preview files without writing them.')
    sp.add_argument('-s', '--strip-comments', action='store_true',
                    help='Remove inline comments from the .SlackBuild body, '
                         'keeping the license header.')

    # download
    sp = sub.add_parser('download', **_kw,
                        help='Download source files listed in the .info file.')
    sp.set_defaults(command='download')
    sp.add_argument('-u', '--check-urls', action='store_true',
                    help='Check URL accessibility with HTTP HEAD requests (no download).')

    # check
    sp = sub.add_parser('check', **_kw,
                        help='Check if package(s) exist in the SBo repository.')
    sp.set_defaults(command='check')
    sp.add_argument('names', metavar='NAME', nargs='+',
                    help='Package name(s) to check.')

    # search
    sp = sub.add_parser('search', **_kw,
                        help='Search for packages in the SBo repository.')
    sp.set_defaults(command='search')
    sp.add_argument('names', metavar='NAME', nargs='+',
                    help='Partial package name(s) to search for.')

    # clean
    sp = sub.add_parser('clean', **_kw,
                        help='Remove downloaded source tarballs.')
    sp.set_defaults(command='clean')

    # info
    sp = sub.add_parser('info', **_kw,
                        help='Display parsed .info file fields.')
    sp.set_defaults(command='info')

    # list
    sp = sub.add_parser('list', **_kw,
                        help='List available SlackBuild templates.')
    sp.set_defaults(command='list')

    # scan
    sp = sub.add_parser('scan', **_kw,
                        help='Scan the source tarball for potential dependencies.')
    sp.set_defaults(command='scan')
    sp.add_argument('tarball', metavar='TARBALL',
                    help='Path to the source tarball to scan.')
    sp.add_argument('-F', '--format', metavar='FORMAT',
                    choices=['python', 'rust', 'perl', 'haskell', 'ruby',
                             'cmake', 'autotools', 'meson'],
                    help='Limit scan to a specific ecosystem.')

    # validate
    sp = sub.add_parser('validate', **_kw,
                        help='Validate .info file fields and md5sums.')
    sp.set_defaults(command='validate')
    sp.add_argument('-m', '--md5', action='store_true',
                    help='Show detailed per-file source status (missing, untracked, checksum).')
    sp.add_argument('-f', '--fix', action='store_true',
                    help='Auto-fix common issues in all package files: trailing whitespace, '
                         'quotes, PRGNAM, HTTP URLs, executable bit and final newline.')
    sp.add_argument('-s', '--strict', action='store_true',
                    help='Run additional pre-submission checks: HTTPS URLs, '
                         'HOMEPAGE accessibility, SlackBuild not executable.')
    sp.add_argument('-A', '--all', action='store_true',
                    help='Validate all packages in the local SBo repo that belong '
                         'to the configured maintainer. Prints a one-line summary per package.')
    sp.add_argument('-M', '--maintainer', metavar='NAME', default=None,
                    help='Override the configured maintainer name (used with --all).')
    sp.add_argument('-p', '--path', metavar='PATH', default=None,
                    help='Path to validate. Without --all: validate that folder instead of '
                         'the current directory. With --all: use as the repo root to scan.')

    # bump
    sp = sub.add_parser('bump', **_kw,
                        help='Bump version in .info and .SlackBuild files.')
    sp.set_defaults(command='bump')
    sp.add_argument('version', metavar='VERSION', help='New version string.')
    sp.add_argument('-W', '--with-sources', action='store_true',
                    help='Update DOWNLOAD URLs, download sources and recompute MD5SUMs.')

    # package
    sp = sub.add_parser('package', **_kw,
                        help='Create the submission tarball (<prgnam>.tar.gz).')
    sp.set_defaults(command='package')
    sp.add_argument('-o', '--output', metavar='DIR',
                    help='Output directory for the tarball.')

    # edit
    sp = sub.add_parser('edit', **_kw,
                        help='Open package files in the configured editor.')
    sp.set_defaults(command='edit')
    sp.add_argument('-e', '--editor', metavar='EDITOR',
                    help='Override the configured editor for this invocation.')

    # rename
    sp = sub.add_parser('rename', **_kw,
                        help='Rename package files and update PRGNAM references.')
    sp.set_defaults(command='rename')
    sp.add_argument('name', metavar='NAME', help='New package name.')

    # fetch
    sp = sub.add_parser('fetch', **_kw,
                        help='Fetch and extract a SlackBuild package from SBo.')
    sp.set_defaults(command='fetch')
    sp.add_argument('name', metavar='NAME', nargs='+',
                    help='Package name to fetch.')
    sp.add_argument('-V', '--slackware-version', metavar='VERSION',
                    help='Slackware version to use (default: auto-detected).')
    sp.add_argument('-l', '--list', action='store_true', default=False,
                    help='List the files inside a package.')
    sp.add_argument('-f', '--file', metavar='FILE', nargs='+', default=None,
                    help='Display one or more files from a package.')
    sp.add_argument('-M', '--maintainer', action='store_true', default=False,
                    help='List packages by maintainer NAME.')
    sp.add_argument('-W', '--wide', action='store_true',
                    help='Show maintainer packages in multi-column layout.')

    # deps
    sp = sub.add_parser('deps', **_kw,
                        help='Show the full dependency tree for a package.')
    sp.set_defaults(command='deps')
    sp.add_argument('names', metavar='NAME', nargs='+', help='Package name(s) to resolve.')
    sp.add_argument('-t', '--tree', action='store_true',
                    help='Show full recursive tree instead of flat list.')

    # rdeps
    sp = sub.add_parser('rdeps', **_kw,
                        help='Show packages that depend on a given package (reverse deps).')
    sp.set_defaults(command='rdeps')
    sp.add_argument('names', metavar='NAME', nargs='+', help='Package name(s) to reverse-resolve.')
    sp.add_argument('-t', '--tree', action='store_true',
                    help='Show full recursive reverse tree instead of flat list.')

    # repo  (groups all local SBo repository operations)
    repo_parser = _SBoCreateParser(
        prog='sbo-create repo', add_help=False,
        formatter_class=_ColorHelpFormatter,
        epilog='For command-specific help: sbo-create repo <COMMAND> --help')
    repo_options = repo_parser.add_argument_group('Options')
    repo_options.add_argument('-N', '--no-color', action='store_true',
                              default=argparse.SUPPRESS, help=argparse.SUPPRESS)
    repo_options.add_argument('-h', '--help', action='help',
                              help='Show this help message and exit.')
    repo_sub = repo_parser.add_subparsers(dest='repo_command', metavar='COMMAND')

    sp = sub.add_parser('repo',
                        parents=[repo_parser],
                        formatter_class=_ColorHelpFormatter,
                        add_help=False,
                        help='Local SBo repository operations.')
    sp.set_defaults(command='repo')

    # repo diff
    rsp = repo_sub.add_parser('diff', **_kw,
                              help='Show diff between local files and the SBo repository.')
    rsp.set_defaults(command='repo', repo_command='diff')
    rsp.add_argument('names', metavar='NAME', nargs='*',
                     help='Package name(s) to diff. Defaults to the current directory.')
    rsp.add_argument('-U', '--unified', metavar='N', type=int, default=3,
                     help='Lines of context around each change (default: 3).')

    # repo sync
    rsp = repo_sub.add_parser('sync', **_kw,
                              help='Sync SBo repo: pull main branch, recreate working branch.')
    rsp.set_defaults(command='repo', repo_command='sync')

    # repo submit
    rsp = repo_sub.add_parser('submit', **_kw,
                              help='Submit package via git or open the SBo submission page.')
    rsp.set_defaults(command='repo', repo_command='submit')

    # repo copy
    rsp = repo_sub.add_parser('copy', **_kw,
                              help='Copy local package files to the SBo repository.')
    rsp.set_defaults(command='repo', repo_command='copy')

    # repo log
    rsp = repo_sub.add_parser('log', **_kw,
                              help='Show packages committed on current branch since last public update.')
    rsp.set_defaults(command='repo', repo_command='log')

    # update
    sp = sub.add_parser('update', **_kw,
                        help='Update templates from slackbuilds.org.')
    sp.set_defaults(command='update')

    # maintainer
    sp = sub.add_parser('maintainer', **_kw,
                        help='Create or update maintainer configuration.')
    sp.set_defaults(command='maintainer')
    sp.add_argument('-S', '--show', action='store_true',
                    help='Show current configuration without prompting.')

    # repology
    sp = sub.add_parser('repology', **_kw,
                        help='Check package versions via repology.org.')
    sp.set_defaults(command='repology')
    sp.add_argument('-n', '--newest', action='store_true',
                    help='Show only up-to-date packages.')
    sp.add_argument('-o', '--outdated', action='store_true',
                    help='Show only outdated packages.')
    sp.add_argument('-p', '--problematic', action='store_true',
                    help='Show only problematic packages.')
    sp.add_argument('-m', '--maintainer', metavar='EMAIL', default=None,
                    help='Maintainer e-mail to query (overrides configured e-mail).')

    parsed = parser.parse_args()

    if not parsed.command:
        parser.print_help()
        raise SystemExit(0)

    if parsed.command == 'repo' and not getattr(parsed, 'repo_command', None):
        repo_parser.print_help()
        raise SystemExit(0)

    return parsed
