#!/usr/bin/env python3
"""Dependency-free source regression gate for SCpanel v1.4.1."""
import ast, re, sys
from pathlib import Path
ROOT=Path(__file__).resolve().parents[1]
APP=ROOT/'app/app.py'
SRC=APP.read_text(encoding='utf-8')
VERSION=(ROOT/'RELEASE_VERSION').read_text().strip()
errors=[]

def check(cond,msg):
    if not cond: errors.append(msg)

check(VERSION=='1.4.1', f'RELEASE_VERSION is {VERSION!r}')
# Route/endpoint uniqueness from AST decorators.
tree=ast.parse(SRC)
paths={}; endpoints={}
for node in tree.body:
    if not isinstance(node,(ast.FunctionDef,ast.AsyncFunctionDef)): continue
    for dec in node.decorator_list:
        if not (isinstance(dec,ast.Call) and isinstance(dec.func,ast.Attribute) and dec.func.attr=='route'): continue
        if not dec.args or not isinstance(dec.args[0],ast.Constant): continue
        path=dec.args[0].value; paths.setdefault(path,[]).append(node.name)
        endpoint=None
        for kw in dec.keywords:
            if kw.arg=='endpoint' and isinstance(kw.value,ast.Constant): endpoint=kw.value.value
        endpoint=endpoint or node.name; endpoints.setdefault(endpoint,[]).append(path)
for p,fs in paths.items(): check(len(set(fs))==1, f'duplicate route path: {p} -> {fs}')
for ep,ps in endpoints.items(): check(len(ps)==1, f'duplicate endpoint: {ep} -> {ps}')
# Critical feature contracts.
for needle,msg in [
    ('def _service_health_snapshot()', 'monitor service health cache helper missing'),
    ('inventory=_service_health_snapshot()', 'monitor inventory helper missing'),
    ("session.clear(); session.update(role=\"admin\",username=\"scroot\",_csrf_token=secrets.token_urlsafe(32))", 'admin return CSRF regeneration missing'),
    ('def _available_php_versions()', 'PHP runtime availability helper missing'),
    ('def background_refresh()', 'service background refresh missing'),
    ("CACHE_FILE=Path('/var/lib/scpanel/service_inventory.json')", 'persistent service inventory missing'),
    ('No verified versions', 'App Store strict repository fallback missing'),
    ('Repository unavailable: install is disabled.', 'App Store install-disable guard missing'),
    ('previous database state preserved', 'website rollback messaging missing'),
    ('Configuration validation failed; previous configuration restored.', 'service config rollback missing'),
    ("'bind9':['named-checkconf','/etc/bind/named.conf']", 'BIND config validator missing'),
]: check(needle in SRC or needle in (ROOT/'app/providers/service_engine.py').read_text() or needle in ''.join(p.read_text(encoding='utf-8') for p in (ROOT/'app/templates').rglob('*.html')), msg)
# v1.3.2 Domains + DNS + service detail contracts.
for needle,msg in [('CREATE TABLE IF NOT EXISTS domains','domains schema missing'),('CREATE TABLE IF NOT EXISTS dns_zones','DNS zones schema missing'),('CREATE TABLE IF NOT EXISTS dns_records','DNS records schema missing'),('def domains_page():','domains page missing'),('def dns_page():','DNS page missing'),('def dns_import(zone_id):','DNS import missing'),('def dns_record_edit(rid):','DNS record edit missing'),("DNS_TYPES={'A','AAAA','CNAME','MX','TXT','NS','SRV','CAA'}",'required DNS record types missing'),('def _ensure_php_repository','multi-version PHP repository setup missing'),('def save_php_settings','PHP settings editor missing'),('def save_fpm_settings','PHP-FPM profile editor missing'),('def service_detail(key):','service detail page missing')]:
    check(needle in SRC or needle in (ROOT/'app/providers/service_engine.py').read_text(), msg)
# v1.4.1 Database Manager + phpMyAdmin contracts.
for needle,msg in [
    ('CREATE TABLE IF NOT EXISTS database_inventory','database inventory schema missing'),
    ('def admin_databases():','admin database manager missing'),
    ('def user_databases():','user database manager missing'),
    ('class DatabaseProvider:','MariaDB/MySQL provider missing'),
    ('class PostgreSQLProvider:','future PostgreSQL provider contract missing'),
    ('def rename_database(old_name, new_name):','database rename provider missing'),
    ('def grant_database(user, host, database, privileges):','database grant provider missing'),
    ('def revoke_database(user, host, database, privileges=None):','database revoke provider missing'),
    ('def _phpmyadmin_installed():','phpMyAdmin detection missing'),
    ('def admin_databases_phpmyadmin():','admin phpMyAdmin opener missing'),
    ('def user_databases_phpmyadmin():','user phpMyAdmin opener missing'),
    ("'phpmyadmin':AppSpec",'phpMyAdmin App Store spec missing'),
    ('Open phpMyAdmin','phpMyAdmin UI opener missing')
]: check(needle in SRC or needle in (ROOT/'app/providers/database_engine.py').read_text() or needle in (ROOT/'app/providers/service_engine.py').read_text() or needle in ''.join(p.read_text(encoding='utf-8') for p in (ROOT/'app/templates').rglob('*.html')), msg)
login=(ROOT/'app/templates/login.html').read_text(encoding='utf-8')
login_css=(ROOT/'app/static/app.css').read_text(encoding='utf-8')
check('login-shell' in login and 'login-form' in login and 'login-button' in login, 'redesigned login template missing')
check('v1.3.4 authentication redesign' in login_css, 'login redesign CSS missing')


# v1.4.1 Email System contracts.
mail=(ROOT/'app/providers/mail_engine.py').read_text(encoding='utf-8')
for needle,msg in [
    ('class MailProviderError','mail provider missing'),
    ('def create_mailbox','mailbox create provider missing'),
    ('def delete_mailbox','mailbox delete provider missing'),
    ('def change_password','mailbox password provider missing'),
    ('def rebuild','Postfix/Dovecot rebuild provider missing'),
    ('def apply_autoresponder','autoresponder provider missing'),
    ('def generate_dkim','DKIM generation workflow missing'),
    ('def configure_dkim','OpenDKIM configuration workflow missing'),
    ("virtual_transport",'Postfix LMTP integration missing'),
    ('99-scpanel.conf','Dovecot SCpanel snippet missing'),
    ('CREATE TABLE IF NOT EXISTS mailboxes','mailbox schema missing'),
    ('CREATE TABLE IF NOT EXISTS mail_forwarders','forwarder schema missing'),
    ("def admin_email():",'admin email manager route missing'),
    ("def user_email():",'user email manager route missing'),
    ('def admin_email_dkim():','admin DKIM route missing'),
    ('def admin_email_dns_records():','SPF/DMARC DNS workflow missing'),
    ("'opendkim':AppSpec",'OpenDKIM App Store spec missing'),
    ('POSTFIX + DOVECOT','mail system UI missing')]:
    check(needle in SRC or needle in mail or needle in (ROOT/'app/providers/service_engine.py').read_text() or needle in ''.join(p.read_text(encoding='utf-8') for p in (ROOT/'app/templates').rglob('*.html')), msg)

# v1.4.1 File Manager contracts.
fm=(ROOT/'app/providers/file_engine.py').read_text(encoding='utf-8')
for needle,msg in [
    ('class FileManagerError','file manager provider missing'),
    ('def safe_path','filesystem isolation guard missing'),
    ('Path traversal is not allowed.','path traversal protection missing'),
    ('Access outside the hosting root is blocked.','symlink/root isolation protection missing'),
    ('def list_dir','file listing missing'),
    ('def upload','upload support missing'),
    ('def copy_item','copy support missing'),
    ('def move_item','move support missing'),
    ('def zip_items','ZIP creation missing'),
    ('def extract_zip','ZIP extraction missing'),
    ('def chmod','permission management missing'),
    ('def chown','ownership management missing'),
    ('def search','file search missing'),
    ('def read_text','editor read support missing'),
    ('def write_text','editor save support missing'),
    ('def admin_files():','admin file manager route missing'),
    ('def user_files():','user file manager route missing'),
    ('def admin_files_download():','admin download route missing'),
    ('def user_files_download():','user download route missing'),
    ('def admin_files_editor():','admin editor route missing'),
    ('def user_files_editor():','user editor route missing'),
    ('/admin/files/action','admin file action endpoint missing'),
    ('/user/files/action','user file action endpoint missing'),
    ('File Manager','file manager UI missing')]:
    check(needle in SRC or needle in fm, msg)
check((ROOT/'app/templates/file_manager.html').exists(), 'file manager template missing')
check((ROOT/'app/templates/file_editor.html').exists(), 'file editor template missing')
check('v1.3.4 File Manager' in (ROOT/'app/static/app.css').read_text(encoding='utf-8'), 'file manager CSS missing')

# v1.4.1 FTP / SFTP contracts.
ftp=(ROOT/'app/providers/ftp_engine.py').read_text(encoding='utf-8')
for needle,msg in [
    ('class TransferProviderError','FTP/SFTP provider missing'),
    ('def create_ftp','FTP account creation missing'),
    ('def create_sftp','SFTP account creation missing'),
    ('def change_password','transfer password management missing'),
    ('def disable_user','transfer disable lifecycle missing'),
    ('def enable_user','transfer enable lifecycle missing'),
    ('def safe_dir','transfer directory isolation missing'),
    ('Directory traversal is not allowed.','transfer traversal protection missing'),
    ('def quota_apply','transfer quota integration missing'),
    ('setquota','filesystem quota support missing'),
    ('def add_key','SFTP SSH key management missing'),
    ('internal-sftp','restricted SFTP shell missing'),
    ('ChrootDirectory','SFTP chroot restriction missing'),
    ('userlist_file=/etc/vsftpd/scpanel-users','FTP allow-list missing'),
    ('CREATE TABLE IF NOT EXISTS ftp_accounts','FTP schema missing'),
    ('CREATE TABLE IF NOT EXISTS sftp_accounts','SFTP schema missing'),
    ('CREATE TABLE IF NOT EXISTS sftp_keys','SFTP key schema missing'),
    ("def admin_ftp():",'admin FTP manager route missing'),
    ("def user_ftp():",'user FTP manager route missing'),
    ("def admin_sftp_create():",'admin SFTP create route missing'),
    ("def user_sftp_create():",'user SFTP create route missing'),
    ('FTP / SFTP','FTP/SFTP navigation/UI missing')]:
    check(needle in SRC or needle in ftp or needle in ''.join(p.read_text(encoding='utf-8') for p in (ROOT/'app/templates').rglob('*.html')), msg)
check((ROOT/'app/templates/admin/ftp.html').exists(), 'FTP/SFTP template missing')

# Website manager contracts.
web= (ROOT/'app/app.py').read_text()
for needle,msg in [('def websites_page():','website page missing'),('def create_website():','website create missing'),('def edit_website(wid):','website edit missing'),('def toggle_website(wid):','website enable/disable missing'),('def delete_website(wid):','website delete missing'),('def website_ssl(wid):','website SSL task missing')]: check(needle in web,msg)
# P4 UI regression contracts.
all_html='\n'.join(p.read_text(encoding='utf-8') for p in (ROOT/'app/templates').rglob('*.html'))
css=(ROOT/'app/static/app.css').read_text(encoding='utf-8')
js=(ROOT/'app/static/app.js').read_text(encoding='utf-8')
check('/admin/system-rebuild' not in all_html, 'obsolete System Rebuild link remains in templates')
check('System Rebuild' not in SRC, 'obsolete System Rebuild route remains in app source')
check('[hidden]{display:none!important}' in css, 'global hidden/modal display guard missing')
check('data-close-svc' in all_html and 'data-close-task' in all_html and 'data-close-install' in all_html, 'modal close hooks missing')
check('closeAllModals' in js and '},true);' in js, 'global modal close manager missing')
check('known_apps' in SRC and '"apps":known_apps' in SRC, 'monitor installed-app payload missing')
check('renderServices(d.apps)' in (ROOT/'app/templates/admin/monitor.html').read_text(encoding='utf-8'), 'monitor detected apps UI contract missing')
check('d.memory.swap_percent' in (ROOT/'app/templates/admin/monitor.html').read_text(encoding='utf-8'), 'monitor swap UI missing')
check('d.rates.read_bps' in (ROOT/'app/templates/admin/monitor.html').read_text(encoding='utf-8'), 'monitor disk IO rate UI missing')
check('d.rates.rx_bps' in (ROOT/'app/templates/admin/monitor.html').read_text(encoding='utf-8'), 'monitor network rate UI missing')
check('swap_percent' in SRC and 'read_bps' in SRC and 'rx_bps' in SRC, 'monitor live rate payload missing')
check(('SERVICE HEALTH' in (ROOT/'app/templates/admin/monitor.html').read_text(encoding='utf-8')) and ('Installed services' in (ROOT/'app/templates/admin/monitor.html').read_text(encoding='utf-8')), 'monitor service health heading missing')

# v1.4.1 SSL Manager contracts.
ssl_html=(ROOT/'app/templates/admin/ssl.html').read_text(encoding='utf-8')
ssl_task=(ROOT/'app/task_engine.py').read_text(encoding='utf-8')
for needle,msg in [
    ('CREATE TABLE IF NOT EXISTS ssl_certificates','SSL certificate schema missing'),
    ('def _ssl_info(domain):','certificate metadata parser missing'),
    ('def admin_ssl():','admin SSL manager route missing'),
    ('def user_ssl():','user SSL manager route missing'),
    ('def _ssl_operation','SSL durable task operation missing'),
    ("'certbot','renew'",'Certbot renew integration missing'),
    ("'certbot','revoke'",'Certbot revoke integration missing'),
    ("'certbot','delete'",'Certbot delete integration missing'),
    ('force_https','Force HTTPS integration missing'),
    ('auto-renew','automatic renewal control missing'),
]: check(needle in SRC or needle in ssl_task or needle in ssl_html,msg)
check('days_left' in ssl_html and 'expires_at' in ssl_html,'SSL expiry countdown UI missing')
check('Issue Let' in ssl_html and 'Renew' in ssl_html and 'Revoke' in ssl_html and 'Delete' in ssl_html,'SSL lifecycle UI incomplete')
check((ROOT/'scripts/scpanel-ssl-renew').exists() and (ROOT/'scripts/scpanel-certbot-deploy-hook').exists(),'SSL renewal scripts missing')
check((ROOT/'scripts/scpanel-ssl-renew.timer').exists(),'SSL renewal timer missing')
check('scpanel-ssl-renew.timer' in (ROOT/'scripts/install-local.sh').read_text(encoding='utf-8'),'installer does not enable SSL renewal timer')


# v1.4.1 Firewall + Port Manager contracts.
fw=(ROOT/'app/providers/firewall_engine.py').read_text(encoding='utf-8')
fw_html=(ROOT/'app/templates/admin/firewall.html').read_text(encoding='utf-8')
for needle,msg in [
    ('CREATE TABLE IF NOT EXISTS firewall_rules','firewall rule schema missing'),
    ('def validate_port','port validation missing'),
    ('def validate_source','source CIDR validation missing'),
    ('SAFE_PROTO','TCP/UDP protocol validation missing'),
    ('def sync_panel_ports','systemd panel port synchronization missing'),
    ('def protect_port','panel lockout protection missing'),
    ("if shutil.which('ufw')",'UFW provider missing'),
    ("if shutil.which('firewall-cmd')",'firewalld provider missing'),
    ("def admin_firewall():",'firewall manager route missing'),
    ("def admin_firewall_create():",'firewall create route missing'),
    ("def admin_firewall_edit(rid):",'firewall edit route missing'),
    ("def admin_firewall_action(rid):",'firewall action route missing'),
    ('TCP/UDP','firewall protocol UI missing'),
    ('PROTECTED','protected panel port UI missing'),
    ('2086 / 2087','admin panel ports UI missing'),
    ('2082 / 2083','user panel ports UI missing'),
]: check(needle in SRC or needle in fw or needle in fw_html,msg)
check((ROOT/'scripts/redirect_http.py').exists(),'HTTP redirect helper missing')
check('scpanel-user-http.service' in (ROOT/'scripts/install-local.sh').read_text(encoding='utf-8'),'user HTTP systemd redirect missing')

print(f'PASS: version={VERSION}; routes={len(paths)}; endpoints={len(endpoints)}')
if errors:
    print('FAIL')
    for e in errors: print(' -',e)
    sys.exit(1)
print('PASS: critical regression contracts')

# v1.4.1 Update Manager contracts
up=(ROOT/'app/providers/update_engine.py').read_text()
for needle,msg in [('def fetch_manifest','update check missing'),('def download','update download missing'),('sha256','SHA256 verification missing'),('def prepare_backup','pre-update backup missing'),('def apply','update apply missing'),('def rollback','rollback missing'),('def verify_runtime','runtime version consistency missing'),('Package version mismatch','package/config mismatch guard missing')]: check(needle in up,msg)
check("('Update Manager','/admin/updates',False)" in SRC,'Update Manager navigation missing')
check("@app.route('/admin/updates')" in SRC,'Update Manager route missing')
