"""Первичная оценка сайта для восстановления: что есть в веб-архиве, кто держит домен, жив ли сайт.

    python -I -X utf8 scripts/survey.py example.ru [папка-результата]

Ничего не ставит, нужен только Python 3. Пишет в папку (по умолчанию ./survey-<домен>/):
  cdx.json     — индекс Wayback по домену
  html.json    — все снимки HTML-страниц (для выбора последнего)
  pages/       — последние снимки HTML-страниц (как есть, в исходной кодировке)
  survey.md    — сводка: домен и DNS, сайт сейчас, страницы, картинки/видео в архиве и их покрытие

Wayback ограничивает частоту запросов: скрипт делает паузы и повторы, повторный запуск докачивает недостающее.
"""
import json, os, re, socket, sys, time, urllib.request
from collections import Counter

UA = 'Mozilla/5.0 (site recovery survey)'
MEDIA = r'(?:jpe?g|png|webp|gif|svg|mp4|webm|pdf)'


def get(url, tries=5, timeout=90):
    delay = 5
    for _ in range(tries):
        try:
            req = urllib.request.Request(url, headers={'User-Agent': UA})
            with urllib.request.urlopen(req, timeout=timeout) as r:
                return r.read()
        except urllib.error.HTTPError as e:
            if e.code == 404:
                return None
        except Exception:
            pass
        time.sleep(delay)
        delay = min(delay * 2, 90)
    return None


def whois_ru(domain):
    if not domain.endswith(('.ru', '.su', '.рф')):
        return 'whois: зона не .ru — смотрите вручную (who.is / whois.com)'
    try:
        s = socket.create_connection(('whois.tcinet.ru', 43), 20)
        s.send(domain.encode() + b'\r\n')
        data = b''
        while chunk := s.recv(4096):
            data += chunk
        keep = ('nserver', 'registrar', 'paid-till', 'free-date', 'state', 'created')
        return '\n'.join(l for l in data.decode('utf-8', 'ignore').splitlines() if l.split(':')[0].strip() in keep)
    except Exception as e:
        return f'whois не ответил: {e}'


def decode(b):
    head = b[:3000].decode('ascii', 'ignore').lower()
    enc = 'cp1251' if ('windows-1251' in head or 'cp1251' in head) else 'utf-8'
    return b.decode(enc, 'ignore')


def main():
    if len(sys.argv) < 2:
        print(__doc__); return
    domain = sys.argv[1].lower().removeprefix('https://').removeprefix('http://').strip('/')
    out = sys.argv[2] if len(sys.argv) > 2 else f'survey-{domain}'
    os.makedirs(os.path.join(out, 'pages'), exist_ok=True)
    rep = [f'# Оценка {domain}', f'Дата: {time.strftime("%Y-%m-%d")}', '']

    rep += ['## Домен и DNS', '```', whois_ru(domain), '```']
    try:
        ip = socket.gethostbyname(domain)
    except Exception:
        ip = None
    rep.append(f'IP сейчас: {ip or "не резолвится"}')
    try:
        req = urllib.request.Request(f'https://{domain}/', headers={'User-Agent': UA})
        code = urllib.request.urlopen(req, timeout=20).status
    except urllib.error.HTTPError as e:
        code = e.code
    except Exception as e:
        code = f'не отвечает ({type(e).__name__})'
    rep += [f'Сайт сейчас: {code}', '']

    cdx_file = os.path.join(out, 'cdx.json')
    if not os.path.exists(cdx_file):
        raw = get(f'http://web.archive.org/cdx/search/cdx?url={domain}&matchType=domain&output=json'
                  '&fl=timestamp,original,mimetype,statuscode,length&collapse=urlkey&limit=50000', timeout=180)
        if not raw:
            sys.exit('Wayback не отдал индекс — повторите позже')
        open(cdx_file, 'wb').write(raw)
    rows = (json.load(open(cdx_file)) or [[]])[1:]
    if not rows:
        rep.append('В веб-архиве ничего нет.')
        open(os.path.join(out, 'survey.md'), 'w', encoding='utf-8').write('\n'.join(rep)); print('\n'.join(rep)); return
    years = Counter(r[0][:4] for r in rows)
    rep += ['## Веб-архив', f'Уникальных адресов: {len(rows)}; снимки с {min(years)} по {max(years)} год.',
            'По годам: ' + ', '.join(f'{y}: {n}' for y, n in sorted(years.items())),
            'По типам: ' + ', '.join(f'{t}: {n}' for t, n in Counter(r[2] for r in rows).most_common(10)), '']

    archived = {re.sub(r'^https?://(www\.)?[^/]+', '', r[1]).split('?')[0] for r in rows if r[3] == '200'}
    html_file = os.path.join(out, 'html.json')
    if not os.path.exists(html_file):
        raw = get(f'http://web.archive.org/cdx/search/cdx?url={domain}&matchType=domain&output=json'
                  '&fl=timestamp,original,mimetype,statuscode,length&filter=mimetype:text/html'
                  '&filter=statuscode:200&limit=100000', timeout=240)
        open(html_file, 'wb').write(raw or b'[]')
    latest = {}  # последний настоящий снимок каждой страницы (заглушки и ошибки короче 2 КБ отбрасываем)
    for r in (json.load(open(html_file)) or [[]])[1:] or [r for r in rows if r[2] == 'text/html' and r[3] == '200']:
        if not (r[4].isdigit() and int(r[4]) > 2000):
            continue
        key = re.sub(r'^https?://(www\.)?', '', r[1]).split('?')[0].rstrip('/').lower()
        if key not in latest or r[0] > latest[key][0]:
            latest[key] = r
    pages = sorted(latest.values(), key=lambda r: r[1])
    rep += ['## Страницы', '| Снимок | Адрес | Заголовок | Медиа в архиве |', '|---|---|---|---|']
    all_media = set()
    for ts, url, *_ in pages:
        fn = os.path.join(out, 'pages', re.sub(r'[^a-z0-9]+', '_', url.lower())[-90:] + '.html')
        if not os.path.exists(fn):
            body = get(f'http://web.archive.org/web/{ts}id_/{url}')
            if body is None:
                rep.append(f'| {ts[:8]} | {url} | не скачалась | |'); continue
            open(fn, 'wb').write(body); time.sleep(1.5)
        t = decode(open(fn, 'rb').read())
        title = re.sub(r'\s+', ' ', (re.findall(r'<title>(.*?)</title>', t, re.S | re.I) or [''])[0]).strip()[:70]
        media = {re.sub(r'^https?://(www\.)?[^/]+', '', m).split('?')[0]
                 for m in re.findall(rf'["\'(]((?:https?://[^"\'()]*{re.escape(domain)})?/[^"\'()\s>]+\.{MEDIA})', t, re.I)}
        all_media |= media
        rep.append(f'| {ts[:8]} | {url} | {title} | {len(media & archived)} из {len(media)} |')
    have = all_media & archived
    rep += ['', '## Картинки, видео, документы',
            f'Упомянуто на страницах: **{len(all_media)}**, есть в архиве: **{len(have)}** '
            f'({round(100 * len(have) / len(all_media)) if all_media else 0}%).',
            'Упомянуто по типам: ' + ', '.join(f'{k}: {v}' for k, v in Counter(m.rsplit(".", 1)[1].lower() for m in all_media).items()),
            '', 'Ссылки на внутренние страницы, которых нет в архиве (кандидаты в потери):']
    linked = set()
    for f in os.listdir(os.path.join(out, 'pages')):
        linked |= set(re.findall(r'href="(/[a-z0-9][^"#?]*/)"', decode(open(os.path.join(out, 'pages', f), 'rb').read())))
    have_pages = {re.sub(r'^https?://(www\.)?[^/]+', '', r[1]).split('?')[0] for r in pages}
    lost = sorted(l for l in linked if l not in have_pages and not l.startswith(('/upload', '/bitrix', '/local', '/wp-')))
    rep += [f'- {l}' for l in lost] or ['- нет']
    open(os.path.join(out, 'survey.md'), 'w', encoding='utf-8').write('\n'.join(rep) + '\n')
    print('\n'.join(rep))


if __name__ == '__main__':
    main()
