#!/usr/bin/python3

from __future__ import annotations

from dataclasses import dataclass
from enum import Enum
from typing import Any, Iterable
import os
import sys

import neaty.log as LOG
from clapp import BaseApp
from clapp.cli import (
    Bool,
    C_NONOPT,
    ListPos,
    LiteralPos,
    MaybeScalarPos,
    P_HELP,
    Pattern,
    ScalarPos,
)
from clapp.sh import (
    quote,
    quote_words,
    stdout_of_code,
)

from saturnin.context import get_config_repo

AttrsT = dict[str, Any]


def identity_fn(arg: Any) -> Any:
    return arg


class Platform(Enum):
    GUI = 'gui'
    TTY = 'tty'


@dataclass(frozen=True, slots=True)
class Role:
    platform: Platform
    name: str

    @classmethod
    def from_text(cls,
                  text: str,
                  default_platform: Platform | None = None,
                  ):
        parts = text.split('.', maxsplit=1)
        LOG.debugv('Role.from_text():parts', parts)
        if len(parts) == 1 and default_platform:
            return cls(default_platform, parts[0])
        if len(parts) == 1:
            raise ValueError(f"platform not known (default was not provided): text={text!r}")
        if len(parts) == 2:
            return cls(Platform(parts[0]), parts[1])

    def fmt(self) -> str:
        return f"{self.platform.value}.{self.name}"


@dataclass(frozen=True, slots=True)
class App:
    role: Role
    cmd: str

    @classmethod
    def from_text(cls,
                  text: str,
                  ):
        parts = text.split(':', maxsplit=1)
        if len(parts) != 2:
            raise ValueError(f"invalid app specification (need PLATFORM.ROLE:CMD): {text!r}")
        return cls(
            role=Role.from_text(parts[0]),
            cmd=parts[1],
        )

    def fmt(self) -> str:
        return f"{self.role.fmt()}:{self.cmd}"


def parse_apps(prefs: dict[str, list[str]]) -> Iterable[App]:
    for role in sorted(prefs.keys()):
        parsed_role = Role.from_text(role)
        for cmd in prefs[role]:
            yield App(parsed_role, cmd)


def check_installed(apps: list[App]) -> Iterable[App]:
    def querycmd(app: App) -> str:
        return (
            f'USERRAPP_ROLE={quote(app.role.fmt())};'
            f' USERRAPP_CMD={quote(app.cmd)};'
            f' command -v "$USERRAPP_CMD" >/dev/null 2>/dev/null'
            f' && echo "$USERRAPP_ROLE:$USERRAPP_CMD";'
        )
    code_lines = [querycmd(app) for app in apps] + ['true']
    for cl in code_lines:
        LOG.debugv('check_installed():cl', cl)
    code = ''.join(cl + '\n' for cl in code_lines)
    LOG.debugv('code', code)
    for line in stdout_of_code(code).splitlines():
        LOG.debugv('check_installed():line', line)
        yield App.from_text(line)


def find_apps(prefs: dict[str, list[str]],
              only_installed: bool = False,
              only_one: bool = False,
              only_role: Role | None = None,
              ) -> Iterable[App]:

    def flt_role(apps: Iterable[App]):
        if only_role is None:
            yield from apps
            return
        for app in apps:
            if app.role != only_role:
                continue
            yield app

    def flt_installed(apps: Iterable[App]):
        LOG.debugv('find_apps():flt_installed():apps', apps)
        if not only_installed:
            yield from apps
            return
        aaa = list(apps)
        LOG.debugv('find_apps():flt_installed():aaa', aaa)
        yield from check_installed(aaa)

    LOG.debugv('find_apps():only_installed', only_installed)
    LOG.debugv('find_apps():only_role', only_role)
    LOG.debugv('find_apps():only_one', only_one)
    for app in flt_installed(flt_role(parse_apps(prefs))):
        yield app
        if only_one:
            return


def select_installed(prefs: dict[str, list[str]],
                     role: Role,
                     ) -> App | None:
    found_iter = iter(find_apps(
        prefs=prefs,
        only_installed=True,
        only_role=role,
        only_one=True,
    ))
    try:
        return next(found_iter)
    except StopIteration:
        return None


def list_apps_a(params: dict, app: _App) -> int:
    found_iter = iter(find_apps(
        prefs=params['prefs'],
        only_installed=not params.get('ALL', False),
        only_role=params.get('role'),
    ))
    for user_app in found_iter:
        print(user_app.fmt())
    return 0


def list_roles_a(params: dict, app: _App) -> int:
    prefs = params['prefs']
    LOG.debugv('prefs', prefs)
    for role in sorted(prefs.keys()):
        print(role)
    return 0


def select_app_a(params: dict, app: _App) -> int:
    role = params['role']
    maybe_app = select_installed(
        prefs=params['prefs'],
        role=role,
    )
    if maybe_app is None:
        LOG.warn(f"no app found for specified role: {role.fmt()}")
        return 1
    print(maybe_app.cmd)
    return 0


def run_app_a(params: dict, app: _App) -> int:
    role = params['role']
    args = params['ARGS']
    dry = params.get('DRY', False)
    maybe_app = select_installed(
        prefs=params['prefs'],
        role=role,
    )
    LOG.debugv('run_app_a():dry', dry)
    LOG.debugv('run_app_a():maybe_app', maybe_app)
    if maybe_app is None:
        LOG.warn(f"no app found for specified role: {role.fmt()}")
        return 1
    cmd = [maybe_app.cmd] + args
    LOG.debugv('run_app_a():cmd', cmd)
    if dry:
        print(' '.join(quote_words(cmd)))
        return 0
    sys.stdout.flush()
    sys.stderr.flush()
    os.execvp(maybe_app.cmd, cmd)


class _App(BaseApp):

    name: str = 'bmo sensible'

    DEFAULT_OPTIONS: AttrsT = {
        'default_platform': Platform.TTY,
    }

    @property
    def arg_scheme(self):
        S = Pattern()
        S.add_pattern(P_HELP)
        S.add_pattern(Pattern(
            options={
                'ALL': Bool('-a|--all'),
                'DRY': Bool('-n|--dry'),
            },
            positionals=[
                ('ROLE', ScalarPos(cond=C_NONOPT)),
                ('ARGS', ListPos()),
            ],
            trigger={'clapp.action': run_app_a}
        )),
        S.add_pattern(Pattern(
            options={
                'ALL': Bool('-a|--all'),
            },
            positionals=[
                ('_', LiteralPos('-l|--list')),
                ('ROLE', MaybeScalarPos()),
            ],
            trigger={'clapp.action': list_apps_a}
        )),
        S.add_pattern(Pattern(
            positionals=[
                ('_', LiteralPos('-L|--list-roles')),
                ('PLATFORM', MaybeScalarPos(factory=Platform)),
            ],
            trigger={'clapp.action': list_roles_a}
        )),
        S.add_pattern(Pattern(
            positionals=[
                ('_', LiteralPos('-s|--select')),
                ('ROLE', ScalarPos()),
            ],
            trigger={'clapp.action': select_app_a}
        )),
        return S

    def init_params(self, from_args: AttrsT) -> AttrsT:
        out = self.DEFAULT_OPTIONS.copy()
        if 'BMO_SENSIBLE_MODE' in os.environ:
            out['default_platform'] = Platform(os.environ['BMO_SENSIBLE_MODE'])
        out.update(from_args)
        if out.get('ROLE'):
            out['role'] = Role.from_text(
                text=out['ROLE'],
                default_platform=out['default_platform'],
            )
        out['prefs'] = get_config_repo().data('sensible')
        return out

    @property
    def usage_patterns(self) -> list[str]:
        return [
            "[-a|--all] [-n|--dry] [gui.|tty.]<browser|terminal|editor|pager|...>",
            "[-a|--all] -l|--list [gui.|tty.][<browser|...>]",
            "-L|--list-roles [gui|tty]",
            "-s|--select [gui.|tty.]<browser|terminal|...>",
        ]

    @property
    def help_lines(self) -> list[str]:
        return self.base_usage + [
        ]


if __name__ == "__main__":
    _App.main()
