#!/usr/bin/env python3
"""Velox command guide and Linux system information; standard library only."""
import argparse
import os
from pathlib import Path
import platform
import shutil
import time

HELP = '''VELOX — Commands help

commands help       Este guia
commands info       Resumo estilo neofetch: logo da distro, CPU, RAM, processos
commands list       Todos os executaveis disponiveis no seu PATH

Sugestoes opcionais (Bash, apenas apos comando nao encontrado)
  source ~/.local/share/velox/shell/bash.sh
  velox_suggestions_off  Desativar e restaurar o handler anterior
  A correcao nunca e executada automaticamente.

Terminal
  Ctrl+Shift+T       Nova sessao (tambem pelo + no topo)
  Ctrl+Shift+W       Fechar aba
  Ctrl+PageUp/Down   Alternar abas
  Ctrl+Shift+D       Dividir painel
  Ctrl+Tab           Alternar painel
  Ctrl+Shift+,       Opcoes integradas: Aparencia / Atalhos
  Ctrl+Shift+P       Paleta de comandos
  Ctrl+Shift+H       Commands help
  Ctrl+Shift+O       Editar arquivo de configuracao
  Ctrl+Shift+Y       Proximo tema
  Ctrl+Shift+C/V     Copiar / colar
  Ctrl+Shift+F       Buscar historico
  Ctrl+Shift+R/S     Reload / salvar configuracao
  Ctrl+Plus/Minus/0  Fonte + / - / reset
  F11               Fullscreen
  Ctrl+C            Interromper programa; Ctrl+D encerra shell
  Ctrl+clique       Abrir link HTTP/HTTPS
  Atalhos acima sao defaults; personalizados ficam em Opcoes > Atalhos.

Linux: consultas uteis (nao sao executadas automaticamente)
  uname -a          Kernel e arquitetura
  cat /etc/os-release  Distribuicao Linux instalada
  lscpu             CPU, nucleos e arquitetura
  free -h           RAM e swap
  df -h             Espaco em disco
  lsblk             Discos, particoes e pontos de montagem
  uptime            Tempo ligado e carga
  ps aux            Processos
  top / htop        Uso de CPU/RAM por processo (htop se instalado)
  ip address        Interfaces de rede
  ip route          Rotas de rede
  ls --color        Arquivos com cores
  pwd / cd / mkdir  Diretorio atual / navegar / criar pasta
  man COMANDO       Manual (se man e paginas instalados)
  COMANDO --help    Ajuda de um programa
  fastfetch / neofetch  Resumo visual do sistema (se instalado)

Dica: commands list mostra o que realmente existe nesta maquina.
A imagem ISO original de instalacao nao pode ser deduzida com seguranca;
commands info identifica o sistema instalado.
'''

def cpu_ticks():
    fields = Path('/proc/stat').read_text().splitlines()[0].split()[1:]
    ticks = [int(x) for x in fields]
    return sum(ticks[:8]), ticks[3] + (ticks[4] if len(ticks) > 4 else 0)

def clean(text):
    """Untrusted /proc/hostname/environment fields must not inject escapes."""
    return ''.join(c if c.isprintable() else '?' for c in str(text))


def logo_for(release, environment=None):
    environment = os.environ if environment is None else environment
    identity = release.get('ID', '').lower()
    session = ' '.join(environment.get(key, '') for key in ('DESKTOP_SESSION', 'XDG_SESSION_DESKTOP', 'XDG_CONFIG_DIRS')).lower()
    if identity == 'ubuntu' and 'lubuntu' in session:
        identity = 'lubuntu'
    path = Path(__file__).resolve()
    locations = (path.parent.parent/'assets/neofetch/logos.json',
                 path.parent.parent/'share/velox/assets/neofetch/logos.json')
    import json
    for location in locations:
        try:
            logos = json.loads(location.read_text())
            break
        except (OSError, ValueError):
            continue
    else:
        return {'art': ['Linux'], 'colors': [36]}, identity or 'linux'
    return logos.get(identity, logos['linux']), identity or 'linux'


def process_snapshot(root=Path('/proc')):
    """Names/ticks/start time/RSS only; no command arguments or subprocesses."""
    result = {}
    page_kib = os.sysconf('SC_PAGE_SIZE') // 1024
    try:
        entries = list(root.iterdir())
    except OSError:
        return result
    for entry in entries:
        if not entry.name.isdigit():
            continue
        try:
            stat = (entry/'stat').read_text()
            name, fields = stat[stat.index('(')+1:].rsplit(')', 1)
            fields = fields.split()
            result[int(entry.name)] = (clean(name), int(fields[11])+int(fields[12]), int(fields[19]), max(0, int(fields[21]))*page_kib)
        except (OSError, ValueError, IndexError):
            continue  # exited process or inaccessible /proc entry
    return result


def top_processes(before, after, elapsed, limit=5):
    frequency = os.sysconf('SC_CLK_TCK')
    rows = []
    for pid, (name, ticks, started, memory) in after.items():
        previous = before.get(pid)
        # A reused PID must not inherit the old process's tick count.
        delta = max(0, ticks-previous[1]) if previous and previous[2] == started else 0
        cpu = delta / frequency / elapsed * 100 if elapsed > 0 else 0
        rows.append((pid, name, cpu, memory))
    return sorted(rows, key=lambda row: (-row[2], -row[3], row[0]))[:limit]


def fetch_lines(logo, facts, columns=80, color=False):
    import re
    import textwrap
    import unicodedata
    marker = re.compile(r'\$\{c([1-6])\}')
    def width(text):
        return sum(0 if unicodedata.combining(c) else 2 if unicodedata.east_asian_width(c) in ('W', 'F') else 1 for c in text)
    def plain(line):
        return marker.sub('', line)
    current_color = logo['colors'][0]
    def painted(line):
        nonlocal current_color
        prefix = '\033['+str(current_color)+'m' if color else ''
        def replace(match):
            nonlocal current_color
            current_color = logo['colors'][min(int(match[1])-1, len(logo['colors'])-1)]
            return '\033['+str(current_color)+'m' if color else ''
        return prefix+marker.sub(replace, line)
    art = logo['art']
    logo_width = max(width(plain(line)) for line in art)
    side_by_side = columns >= logo_width+24
    fact_width = max(1, columns-logo_width-3) if side_by_side else max(1, columns)
    right = []
    for label, value in facts:
        label, value = clean(label), clean(value)
        text = label+(': ' if label else '')+value
        wrapped = textwrap.wrap(text, width=fact_width, subsequent_indent='  ' if fact_width >= 4 else '', break_long_words=True, break_on_hyphens=False) or ['']
        for line in wrapped:
            part = ''; used = 0
            for char in line:
                size = width(char)
                if part and used+size > fact_width:
                    right.append(part); part = ''; used = 0
                part += char; used += size
            right.append(part)
    reset = '\033[0m' if color else ''
    accent = '\033[1;'+str(logo['colors'][0])+'m' if color else ''
    def fact_color(line, index):
        if not color:
            return line
        if ':' in line:
            label, value = line.split(':', 1)
            return accent+label+reset+':'+value
        return accent+line+reset if index < 2 else line
    if side_by_side:
        for i in range(max(len(art), len(right))):
            left = art[i] if i < len(art) else ''
            yield painted(left)+reset+' '*(logo_width-width(plain(left))+3)+(fact_color(right[i], i) if i < len(right) else '')
    else:
        for line in art:
            # Small windows get vertical layout and no wrapped logo corruption.
            if width(plain(line)) > columns:
                yield plain(line)[:max(1, columns)]
            else:
                yield painted(line)+reset
        yield ''
        for i, line in enumerate(right):
            yield fact_color(line, i)


def info(color=None):
    if hasattr(platform, 'freedesktop_os_release'):
        release = platform.freedesktop_os_release()
    else:
        release = dict(line.strip().split('=', 1) for line in Path('/etc/os-release').read_text().splitlines() if '=' in line and not line.startswith('#'))
        release = {key: value.strip('"') for key, value in release.items()}
    logo, identity = logo_for(release)
    model = next((line.split(':', 1)[1].strip() for line in Path('/proc/cpuinfo').read_text().splitlines() if line.startswith('model name')), platform.machine())
    mem = {line.split(':')[0]: int(line.split()[1])*1024 for line in Path('/proc/meminfo').read_text().splitlines()}
    processes0 = process_snapshot()
    total0, idle0 = cpu_ticks()
    sample_start = time.monotonic()
    time.sleep(.1)
    processes1 = process_snapshot()
    total1, idle1 = cpu_ticks()
    interval = time.monotonic()-sample_start
    usage = 100*(1-(idle1-idle0)/(total1-total0)) if total1 > total0 else 0
    disk = shutil.disk_usage('/')
    uptime = float(Path('/proc/uptime').read_text().split()[0])
    gib = lambda n: f'{n/1073741824:.2f} GiB'
    username = os.environ.get('USER') or str(os.getuid())
    title = clean(username)+'@'+clean(platform.node())
    name = release.get('PRETTY_NAME', 'Linux')
    if identity == 'lubuntu' and release.get('ID') == 'ubuntu':
        name += ' (sessao Lubuntu)'
    facts = [('', title), ('', '-'*min(len(title), 30)),
        ('OS', name+' '+platform.machine()), ('Host', platform.node()),
        ('Kernel', platform.release()), ('Uptime', f'{int(uptime//3600)}h {int(uptime%3600//60)}min'),
        ('Shell', os.environ.get('SHELL', 'nao informado')), ('DE', os.environ.get('XDG_CURRENT_DESKTOP', 'nao informado')),
        ('Terminal', 'Velox'), ('CPU', model), ('Threads', str(os.cpu_count())),
        ('Uso CPU', f'{usage:.1f}% global, amostra ~{interval*1000:.0f} ms'),
        ('RAM', gib(mem['MemTotal']-mem.get('MemAvailable', mem.get('MemFree', 0)))+' / '+gib(mem['MemTotal'])),
        ('Swap', gib(mem['SwapTotal']-mem['SwapFree'])+' / '+gib(mem['SwapTotal'])),
        ('Disco /', gib(disk.used)+' / '+gib(disk.total))]
    if color is None:
        color = os.isatty(1) and 'NO_COLOR' not in os.environ
    columns = shutil.get_terminal_size((80, 24)).columns
    for line in fetch_lines(logo, facts, columns, color):
        print(line)
    print('\nProcessos principais — CPU na amostra; desempate por RAM')
    print('    PID    CPU%   RAM(MiB)  Processo')
    for pid, process, cpu, memory in top_processes(processes0, processes1, interval):
        print(f'{pid:7d}  {cpu:6.1f}  {memory/1024:9.1f}  {clean(process)}')
    if color:
        print(''.join(f'\033[{40+i}m   ' for i in range(8))+'\033[0m')
    print('')

def executables():
    found = set()
    for directory in os.get_exec_path():
        try:
            for path in Path(directory or '.').iterdir():
                if path.name.isprintable() and path.is_file() and os.access(path, os.X_OK):
                    found.add(path.name)
        except OSError:
            continue
    for name in sorted(found):
        print(name)

def main():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument('command', nargs='?', default='help', choices=('help', 'info', 'list'))
    parser.add_argument('--hold', action='store_true', help='keep guide session open until Enter')
    parser.add_argument('--color', choices=('auto', 'always', 'never'), default='auto', help='fetch logo/info color output')
    args = parser.parse_args()
    if args.command == 'help': print(HELP)
    elif args.command == 'info': info(None if args.color == 'auto' else args.color == 'always')
    else: executables()
    if args.hold:
        try: input('\nEnter para fechar esta sessao... ')
        except (EOFError, KeyboardInterrupt): pass

if __name__ == '__main__':
    main()
