#!/usr/bin/env python3
"""Проверка корневого сайта без сетевых допусков. Отчёт пишется по явному пути."""
from pathlib import Path
from urllib.parse import urlsplit,unquote
from collections import Counter
from bs4 import BeautifulSoup
import hashlib,json,re,subprocess,sys,unicodedata
R=Path(__file__).resolve().parent.parent
rows=[];errors=[];pending=[];mixed=[];external=set();soups={}
for p in sorted(R.rglob('*.html')):
 soups[p]=BeautifulSoup(p.read_text(encoding='utf-8'),'html.parser')
for p,s in soups.items():
 rel=p.relative_to(R).as_posix();bad=[]
 if s.html.get('lang')!='ru':bad.append('Язык документа не ru')
 if s.body.get('data-release')!='4.9.0-wip':bad.append('Нет точной текущей метки wip')
 canon=s.find('link',rel='canonical')
 if not canon or urlsplit(canon.get('href','')).netloc!='xn--80advqd.com':bad.append('Канонический адрес')
 ids=[x['id'] for x in s.select('[id]')]
 if len(ids)!=len(set(ids)):bad.append('Повторные идентификаторы: '+','.join(k for k,v in Counter(ids).items() if v>1))
 for e in s.find_all(True):
  for k in e.attrs:
   if k.startswith('on') or k=='style':bad.append('Встроенный исполняемый атрибут '+k)
 for sc in s.find_all('script'):
  if sc.get('type')=='application/ld+json':
   try:json.loads(sc.string or '')
   except Exception as ex:bad.append('JSON-LD: '+str(ex))
  elif not sc.get('src'):bad.append('Встроенный исполняемый сценарий')
 for img in s.find_all('img'):
  if not img.has_attr('alt'):bad.append('Изображение без alt')
 for e in s.find_all(True):
  for attr in ['href','src']:
   if not e.has_attr(attr):continue
   u=e[attr];v=urlsplit(u)
   if v.scheme in ['mailto','tel','data','blob']:continue
   if v.scheme and v.scheme not in ['http','https']:bad.append('Недопустимая схема ссылки '+u);continue
   if v.netloc and v.netloc not in ['xn--80advqd.com','война.com']:
    external.add(u);continue
   raw=unquote(v.path)
   target=(R/raw.lstrip('/') if raw.startswith('/') or v.netloc else p.parent/raw) if raw else p
   if target.is_dir():target=target/'index.html'
   target=target.resolve()
   if not target.is_relative_to(R):bad.append('Выход ссылки за корень '+u);continue
   if not target.is_file():
    if target.suffix=='.zip':pending.append({'страница':rel,'ссылка':u})
    else:bad.append('Нет файла: '+u)
   elif v.fragment and target.suffix=='.html':
    other=soups.get(target)
    if other and not other.find(id=unquote(v.fragment)) and not other.find('a',attrs={'name':unquote(v.fragment)}):bad.append('Нет якоря: '+u)
 # Visible-script review is a flag list, not an automatic language verdict.
 text=' '.join(x for x in s.strings if x.parent.name not in ['script','style','code','pre'])
 for m in re.finditer(r'[A-Za-zА-Яа-яЁё]{2,}',text):
  word=m.group()
  if re.search('[A-Za-z]',word) and re.search('[А-Яа-яЁё]',word):mixed.append({'страница':rel,'слово':word})
 if re.search(r'[\u202a-\u202e\u2066-\u2069]',text):bad.append('Управляющий символ направления текста')
 rows.append({'страница':rel,'ша256':hashlib.sha256(p.read_bytes()).hexdigest(),'ошибки':list(dict.fromkeys(bad))})
 errors.extend({'страница':rel,'ошибка':x} for x in dict.fromkeys(bad))
js=[]
for p in sorted((R/'ресурсы/сценарии').glob('*.js')):
 x=subprocess.run(['node','--check',str(p)],capture_output=True,text=True)
 js.append({'файл':p.relative_to(R).as_posix(),'пройдено':x.returncode==0,'ошибка':x.stderr.strip()})
 if x.returncode:errors.append({'сценарий':str(p),'ошибка':x.stderr})
# No schema keyword is silently accepted by the independent validator.
import jsonschema
schemas=[]
for n in ['схема-протокола-потока-4.9.0.json','схема-переносимой-записи-аудита-4.9.0.json','схема-продольной-панели-4.9.0.json','схема-папки-дела-4.9.0.json','схема-свидетельства-владельца.json','схема-решения-конфликта-4.9.0.json','схема-реестра-требований-4.9.0.json','схема-события-исправления-4.9.0.json','схема-квитанции-вызова-4.9.0.json','схема-локального-аудита-4.5.0.json','схема-локального-реестра-4.5.0.json']:
 try:jsonschema.Draft202012Validator.check_schema(json.loads((R/'данные'/n).read_text()));schemas.append({'схема':n,'пройдено':True})
 except Exception as e:schemas.append({'схема':n,'пройдено':False,'ошибка':str(e)});errors.append({'схема':n,'ошибка':str(e)})
audit_schema=json.loads((R/'данные/схема-переносимой-записи-аудита-4.3.0.json').read_text());fixtures=[]
for p in sorted((R/'данные/примеры-продольной-серии').glob('*.json')):
 if p.name=='индекс.json':continue
 errs=[str(e) for e in jsonschema.Draft202012Validator(audit_schema).iter_errors(json.loads(p.read_text()))]
 fixtures.append({'файл':p.name,'ошибки':errs})
 if errs:errors.append({'пример':p.name,'ошибка':errs})
mirrors=[]
for n in ['ранний-разбор-и-бюджет', 'строгий-протокол-потока', 'причины-устаревания-событий', 'согласованность-схем-и-версий', 'проверка-рубежей-4-9', 'решение-о-выпуске-4-9']+['продольный-доказательный-контур','папка-дела','распространение-исправлений','реестр-и-граф-требований','словарь-методики','проверка-рубежей-4-4','решение-о-выпуске-4-4','границы-входа-и-схемы','состав-наблюдателей','рассмотрение-конфликтов','ответы-и-остатки-дела','подписанный-реестр','перенос-оболочек','история-решений','временные-срезы-дела','пределы-ресурсов','защита-публикации-ключа','проверка-рубежей-4-5','решение-о-выпуске-4-6','многозвенный-перенос','событийное-исправление','согласованный-перенос-черновика','ресурсный-бюджет-и-отмена','проверка-рубежей-4-6','представления-и-память','этапная-отмена','состав-исправления','квитанция-заявленного-вызова','проверка-рубежей-4-7','решение-о-выпуске-4-7','восстановление-повреждённого-корня','рабочий-поток-и-снимки','ограничительный-профиль','приём-квитанции','проверка-рубежей-4-8','решение-о-выпуске-4-8']:
 paths=[R/(n+'.uai'),R/'.uai'/(n+'.uai'),R/'.uai/память'/(n+'.uai')]
 # Discover exact cold location from tree, without presuming an alternative layout.
 if not paths[-1].is_file():paths[-1]=next((R/'uai').rglob(n+'.uai'),paths[-1])
 ok=all(p.is_file() for p in paths) and len({p.read_bytes() for p in paths})==1
 mirrors.append({'модуль':n,'тождественны':ok,'пути':[str(p.relative_to(R)) for p in paths]})
 if not ok:errors.append({'память':n,'ошибка':'Зеркала не тождественны'})
report={'редакция':'4.9.0-wip','вид':'Локальный статический, ссылочный и схемный контроль','страниц':len(rows),'страницы':rows,'ошибки':errors,'ожидают_сборки_архивов':pending,'смешанные_алфавиты_для_редактора':mixed,'внешних_адресов_без_сетевого_допуска':len(external),'сценарии':js,'схемы':schemas,'искусственные_примеры':fixtures,'зеркала_памяти':mirrors,'граница':'Это не полная ручная корректура, не фактическая проверка утверждений и не сетевой или браузерный допуск. Ссылки в исходных исследованиях не переписываются.'}
if len(sys.argv)>1:Path(sys.argv[1]).write_text(json.dumps(report,ensure_ascii=False,indent=2)+'\n')
print(json.dumps({'страниц':len(rows),'ошибки':errors,'ожидают_архивов':pending,'смешанные_слова':mixed},ensure_ascii=False,indent=2))
sys.exit(1 if errors else 0)
