#!/usr/bin/env python3
"""Didnex Connect v1: inspect locally, download verified SDK, produce canonical guide.
Default is read-only. --apply stages SDK and guide in .didnex; no app code is overwritten.
"""
import argparse, hashlib, json, os, pathlib, re, shutil, subprocess, tempfile, urllib.request, urllib.parse, zipfile

def detect(root):
    if (root/'pubspec.yaml').exists():return 'flutter'
    if list(root.glob('*.b4a')):return 'b4a'
    if (root/'project.godot').exists():return 'godot'
    if (root/'ProjectSettings/ProjectVersion.txt').exists():return 'unity'
    if (root/'package.json').exists():
        p=json.loads((root/'package.json').read_text(encoding='utf-8-sig'))
        if 'react-native' in {**p.get('dependencies',{}),**p.get('devDependencies',{})}:return 'react-native'
    if list(root.glob('*.xcodeproj')) or (root/'Package.swift').exists():return 'ios'
    if (root/'settings.gradle').exists() or (root/'settings.gradle.kts').exists():return 'android'
    if (root/'index.html').exists():return 'web'
    raise ValueError('Project not detected; select --platform explicitly')

def doctor(root,platform):
    checks=[]
    def add(label,ok,note):checks.append({'check':label,'ok':ok,'note':note})
    if platform=='flutter':
        pub=(root/'pubspec.yaml').read_text(encoding='utf-8-sig');add('Flutter',bool(shutil.which('flutter')),'flutter must be installed locally')
        add('Existing dependency','didnex_ads:' in pub,'Use staged sdk/flutter/didnex_ads as path; preserve existing app dependencies')
        sdk=re.search(r'sdk:\s*[\'"]?([^\n\'"]+)',pub);add('Dart constraint',bool(sdk),'SDK needs Dart >=3.6 / Flutter >=3.27; confirm your host toolchain')
    if platform in ('flutter','android','b4a','react-native','godot'):
        manifests=list(root.glob('android/app/src/main/AndroidManifest.xml'))+list(root.glob('app/src/main/AndroidManifest.xml'))
        for p in manifests:
            raw=p.read_text(encoding='utf-8-sig');add('INTERNET permission','android.permission.INTERNET' in raw,str(p.relative_to(root)))
            add('Cleartext disabled','usesCleartextTraffic="true"' not in raw,'HTTPS service origin is required')
        add('Java',bool(shutil.which('java')),'Keep host Gradle/AGP/Kotlin versions; do not clear caches automatically')
    if platform=='b4a':add('B4A classes',(root/'DidnexAds.bas').exists(),'Add staged class modules in B4A IDE and enable required libraries from guide')
    return checks

def main():
    p=argparse.ArgumentParser(description=__doc__);p.add_argument('--project',default='.');p.add_argument('--platform',choices=['web','flutter','android','ios','unity','b4a','react-native','godot']);p.add_argument('--origin',required=True);p.add_argument('--unit',required=True);p.add_argument('--apply',action='store_true');a=p.parse_args()
    root=pathlib.Path(a.project).resolve();platform=a.platform or detect(root);origin=a.origin.rstrip('/');u=urllib.parse.urlsplit(origin)
    if u.scheme!='https' or u.username or u.password or u.path or u.query or u.fragment:raise ValueError('HTTPS origin required')
    if not re.fullmatch(r'UNT_[a-f0-9]{32}',a.unit):raise ValueError('Valid UNT_ placement required')
    class SameOrigin(urllib.request.HTTPRedirectHandler):
        def redirect_request(self,req,fp,code,msg,hdrs,newurl):
            if urllib.parse.urlsplit(newurl).netloc!=u.netloc or not newurl.startswith(origin+'/'):raise ValueError('cross_origin_redirect')
            return super().redirect_request(req,fp,code,msg,hdrs,newurl)
    opener=urllib.request.build_opener(SameOrigin());url=origin+'/api/connect-public?'+urllib.parse.urlencode({'unitId':a.unit,'platform':platform})
    with opener.open(url,timeout=20) as r:info=json.load(r)
    checks=doctor(root,platform);print(json.dumps({'platform':platform,'unit':info['unit'],'checks':checks,'plan':['Stage SHA256-verified SDK under .didnex','Write canonical code and manifest guide','Preserve current app code and Gradle versions'],'applied':a.apply},ensure_ascii=False,indent=2))
    if not a.apply:return
    if not info['sdk']['download'].startswith('/sdk/'):raise ValueError('invalid_sdk_path')
    with tempfile.TemporaryDirectory(prefix='didnex-connect-') as tmp:
        archive=pathlib.Path(tmp)/'sdk';h=hashlib.sha256();total=0
        with opener.open(origin+info['sdk']['download'],timeout=60) as r,open(archive,'wb') as f:
            while True:
                b=r.read(65536)
                if not b:break
                total+=len(b)
                if total>50*1024*1024:raise ValueError('sdk_too_large')
                h.update(b);f.write(b)
        if h.hexdigest()!=info['sdk']['sha256']:raise ValueError('sdk_checksum_mismatch')
        target=root/'.didnex'/('sdk-'+platform+'-'+info['sdk']['version'])
        if target.exists():raise ValueError('SDK folder already exists; keep it or move it aside before staging again')
        stage=pathlib.Path(tmp)/'stage';stage.mkdir()
        if zipfile.is_zipfile(archive):
            with zipfile.ZipFile(archive) as z:
                size=0
                for i in z.infolist():
                    n=pathlib.PurePosixPath(i.filename.replace('\\','/'));size+=i.file_size
                    if n.is_absolute() or '..' in n.parts or ':' in str(n) or (i.external_attr>>16)&0o170000==0o120000 or size>100*1024*1024:raise ValueError('unsafe_archive')
                z.extractall(stage)
        else:shutil.copy2(archive,stage/'didnex.js')
        target.parent.mkdir(exist_ok=True);shutil.copytree(stage,target)
        guide=target.parent/('CONNECT-'+a.unit+'.json');old=guide.with_suffix('.json.backup')
        if guide.exists():shutil.copy2(guide,old)
        guide.write_text(json.dumps(info,ensure_ascii=False,indent=2),encoding='utf-8')
        snippets=target.parent/('GUIDE-'+a.unit+'.md')
        if snippets.exists():shutil.copy2(snippets,snippets.with_suffix('.md.backup'))
        integration=info['integration']
        parts=['# Didnex · '+platform,'SDK staged at: '+str(target),'Only public placement ID and HTTPS origin belong in the app. No secret key is needed.','## Canonical integration','```\n'+integration.get('code','')+'\n```','## Manifest / requirements',json.dumps({k:v for k,v in integration.items() if k!='code'},ensure_ascii=False,indent=2)]
        snippets.write_text('\n\n'.join(parts),encoding='utf-8')
        print('Staged: '+str(target));print('Guide: '+str(snippets));print('Follow the placement connection wizard, run local build, then use its 15-minute diagnostic session. Never credit test events.')
if __name__=='__main__':main()
