#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
soft_tracker.py v1.0 — аудит установленного софта Ubuntu Desktop.
Собирает ХРОНОЛОГИЮ установок и ИСТОЧНИКИ (серверы репозиториев):
  apt/dpkg  -> /var/log/apt/history.log* + /var/log/dpkg.log*
  snap      -> snap changes
  flatpak   -> flatpak list + mtime каталогов /var/lib/flatpak/app
Источник каждого apt-пакета определяется через apt-cache policy.
Вывод: install-history.csv / .json / .md
Запуск: sudo python3 soft_tracker.py [каталог_вывода]
"""
import csv, gzip, json, os, re, subprocess, sys
from datetime import datetime
from pathlib import Path

OUT = Path(sys.argv[1]).expanduser() if len(sys.argv) > 1 else Path.home() / 'soft-tracker' / 'out'
OUT.mkdir(parents=True, exist_ok=True)

def run(cmd):
    p = subprocess.run(cmd, shell=True, capture_output=True, text=True, errors='replace')
    return p.stdout

def read_log(path):
    path = Path(path)
    if not path.exists():
        return ''
    if path.suffix == '.gz':
        with gzip.open(path, 'rt', errors='replace') as f:
            return f.read()
    return path.read_text(errors='replace')

records = []

# ---------- 1) apt: команды пользователя и пакеты ----------
apt_pkgs = set()
files = []
log_dir = Path('/var/log/apt')
if log_dir.exists():
    for f in log_dir.glob('history.log*'):
        m = re.search(r'history\.log(?:\.(\d+))?(?:\.gz)?$', f.name)
        files.append((int(m.group(1)) if m and m.group(1) else 0, f))
for _, f in sorted(files, key=lambda x: -x[0]):            # от старых к новым
    for block in read_log(f).split('\n\n'):
        sd = re.search(r'Start-Date: ([\d\-]+ [\d:]+)', block)
        if not sd:
            continue
        ts = sd.group(1)
        cmd = re.search(r'Commandline: (.*)', block)
        cmd = cmd.group(1).strip() if cmd else ''
        for field in ('Install', 'Upgrade'):
            m = re.search(field + r': (.*)', block)
            if not m:
                continue
            for name, ver in re.findall(r'([a-z0-9][a-z0-9.+\-]*):[a-z0-9_\-]+ \(([^)]*)\)', m.group(1)):
                records.append({'ts': ts, 'package': name, 'version': ver.split(',')[0],
                                'method': 'apt', 'source': '', 'command': cmd,
                                'action': field.lower()})
                apt_pkgs.add(name)

# ---------- 2) dpkg: ручные .deb, не попавшие в apt ----------
dpkg_files = []
for f in Path('/var/log').glob('dpkg.log*'):
    m = re.search(r'dpkg\.log(?:\.(\d+))?(?:\.gz)?$', f.name)
    dpkg_files.append((int(m.group(1)) if m and m.group(1) else 0, f))
for _, f in sorted(dpkg_files, key=lambda x: -x[0]):
    for line in read_log(f).splitlines():
        m = re.match(r'([\d\-]+ [\d:]+) install (\S+?):\S+ <none> (\S+)', line)
        if m and m.group(2) not in apt_pkgs:
            records.append({'ts': m.group(1), 'package': m.group(2), 'version': m.group(3),
                            'method': 'dpkg', 'source': '', 'command': 'dpkg -i <локальный .deb>',
                            'action': 'install'})
            apt_pkgs.add(m.group(2))

# ---------- 3) snap ----------
snap_ver = {}
for line in run('snap list 2>/dev/null').splitlines()[1:]:
    c = line.split()
    if len(c) >= 2:
        snap_ver[c[0]] = c[1]
for line in run('snap changes 2>/dev/null').splitlines()[1:]:
    c = line.split(None, 4)
    if len(c) < 5:
        continue
    m = re.search(r'Install "([^"]+)" snap', c[4])
    if m:
        records.append({'ts': c[2].replace('T', ' ')[:19], 'package': m.group(1),
                        'version': snap_ver.get(m.group(1), '?'), 'method': 'snap',
                        'source': 'snap store (snapcraft.io)',
                        'command': 'snap install ' + m.group(1), 'action': 'install'})

# ---------- 4) flatpak ----------
remote_url = {}
for line in run('flatpak remotes --columns=name,url 2>/dev/null').splitlines():
    if '\t' in line:
        n, u = line.split('\t', 1)
        remote_url[n] = u
for line in run('flatpak list --app --columns=application,version,origin 2>/dev/null').splitlines():
    if '\t' not in line:
        continue
    app, ver, origin = (line.split('\t') + [''] * 3)[:3]
    d = Path('/var/lib/flatpak/app') / app
    ts = datetime.fromtimestamp(d.stat().st_mtime).strftime('%Y-%m-%d %H:%M:%S') if d.exists() else '0000-00-00 00:00:00'
    records.append({'ts': ts, 'package': app, 'version': ver or '?', 'method': 'flatpak',
                    'source': remote_url.get(origin, origin), 'command': 'flatpak install ' + app,
                    'action': 'install', 'approx': True})

# ---------- 5) источники apt/dpkg через apt-cache policy ----------
cache = {}
def source_of(pkg):
    if pkg in cache:
        return cache[pkg]
    out = run('apt-cache policy ' + pkg)
    m = re.search(r'\d+ (https?://\S+|file:\S+)', out)
    if m:
        src = m.group(1)
    elif 'Installed: (none)' not in out:
        src = 'локальный .deb (вне репозиториев)'
    else:
        src = 'неизвестно (пакет удалён)'
    cache[pkg] = src
    return src

for r in records:
    if r['method'] in ('apt', 'dpkg') and not r['source']:
        r['source'] = source_of(r['package'])

records.sort(key=lambda r: r['ts'])
for i, r in enumerate(records, 1):
    r['seq'] = i

# ---------- вывод ----------
host = run('hostname').strip()
codename = run('bash -c ". /etc/os-release && echo $VERSION_CODENAME"').strip()
now = datetime.now().strftime('%Y-%m-%d %H:%M:%S')

with open(OUT / 'install-history.csv', 'w', newline='', encoding='utf-8-sig') as f:
    w = csv.writer(f, delimiter=';')
    w.writerow(['№', 'Дата', 'Пакет', 'Версия', 'Способ', 'Источник', 'Команда'])
    for r in records:
        w.writerow([r['seq'], r['ts'], r['package'], r['version'], r['method'], r['source'], r['command']])

(OUT / 'install-history.json').write_text(json.dumps(records, ensure_ascii=False, indent=1), encoding='utf-8')

md = ['# Отчёт об установках софта — ' + host, '',
      '- **Создан:** ' + now,
      '- **ОС:** Ubuntu ' + codename,
      '- **Всего записей:** ' + str(len(records)), '',
      '## Сводка по способам', '| Способ | Кол-во |', '|---|---|']
for mth in sorted({r['method'] for r in records}):
    md.append('| %s | %d |' % (mth, sum(1 for r in records if r['method'] == mth)))
md += ['', '## Сводка по серверам-источникам', '| Источник | Кол-во |', '|---|---|']
srcs = {}
for r in records:
    srcs[r['source']] = srcs.get(r['source'], 0) + 1
for s, c in sorted(srcs.items(), key=lambda kv: -kv[1]):
    md.append('| %s | %d |' % (s or '—', c))
md += ['', '## Хронология установок',
       '| № | Дата | Пакет | Версия | Способ | Источник | Команда |',
       '|---|---|---|---|---|---|---|']
for r in records:
    md.append('| %d | %s | %s | %s | %s | %s | %s |' % (
        r['seq'], r['ts'], r['package'], r['version'], r['method'],
        r['source'] or '—', r['command'].replace('|', '\\|')[:90]))
(OUT / 'install-history.md').write_text('\n'.join(md) + '\n', encoding='utf-8')

if os.environ.get('SUDO_UID'):   # возвращаем права владельцу после sudo
    uid, gid = int(os.environ['SUDO_UID']), int(os.environ.get('SUDO_GID', 0))
    for p in OUT.iterdir():
        os.chown(p, uid, gid)

print('✅ Отчёты готовы в', OUT)
print('   записей:', len(records))
for p in sorted(OUT.iterdir()):
    print('  -', p.name)
