from pathlib import Path
import xml.etree.ElementTree as ET
import re,sys
root=Path(__file__).resolve().parents[1]
errors=[]
for p in list((root/'res').rglob('*.xml'))+[root/'AndroidManifest.xml',root/'tests/android/AndroidManifest.xml']:
    try: ET.parse(p)
    except Exception as e: errors.append(f'XML {p.relative_to(root)}: {e}')
strings=set(); ids=set(); layouts={p.stem for p in (root/'res/layout').glob('*.xml')}; drawables={p.stem for p in (root/'res/drawable').glob('*')}
for vp in (root/'res').glob('values*/strings.xml'):
    names=[e.attrib.get('name') for e in ET.parse(vp).getroot() if e.tag=='string']
    if len(names)!=len(set(names)): errors.append(f'duplicate string names: {vp.relative_to(root)}')
    strings|=set(names)
for p in (root/'res').rglob('*.xml'):
    ids|=set(re.findall(r'@\+id/([A-Za-z0-9_]+)',p.read_text(errors='ignore')))
for p in (root/'src').rglob('*.java'):
    t=re.sub(r'android\.R\.string\.[A-Za-z0-9_]+','',p.read_text())
    for n in re.findall(r'(?<!android\.)R\.string\.([A-Za-z0-9_]+)',t):
        if n not in strings: errors.append(f'missing string {n}: {p.relative_to(root)}')
    for n in re.findall(r'(?<!android\.)R\.id\.([A-Za-z0-9_]+)',t):
        if n not in ids: errors.append(f'missing id {n}: {p.relative_to(root)}')
    for n in re.findall(r'(?<!android\.)R\.layout\.([A-Za-z0-9_]+)',t):
        if n not in layouts: errors.append(f'missing layout {n}: {p.relative_to(root)}')
    if re.search(r'\b(TODO|FIXME)\b',t): errors.append(f'TODO/FIXME: {p.relative_to(root)}')
for p in (root/'res').rglob('*.xml'):
    t=p.read_text(errors='ignore')
    for n in re.findall(r'@string/([A-Za-z0-9_]+)',t):
        if n not in strings: errors.append(f'missing XML string {n}: {p.relative_to(root)}')
    for n in re.findall(r'@drawable/([A-Za-z0-9_]+)',t):
        if n not in drawables: errors.append(f'missing drawable {n}: {p.relative_to(root)}')
# Android instrumentation runner sanity checks. XML well-formedness alone is not enough.
test_manifest_path=root/'tests/android/AndroidManifest.xml'
test_tree=ET.parse(test_manifest_path).getroot()
ns='{http://schemas.android.com/apk/res/android}'
app=test_tree.find('application')
if app is None: errors.append('test manifest missing application element')
else:
    runner_libs=[e.attrib.get(ns+'name') for e in app.findall('uses-library')]
    if 'android.test.runner' not in runner_libs: errors.append('android.test.runner must be declared inside test application')
instrumentation=test_tree.find('instrumentation')
if instrumentation is None: errors.append('test manifest missing instrumentation')
elif instrumentation.attrib.get(ns+'name')!='android.test.InstrumentationTestRunner': errors.append('unexpected instrumentation runner')

bp=(root/'Android.bp').read_text(errors='ignore')
for lib in ('android.test.base.stubs.system','android.test.mock.stubs.system','android.test.runner.stubs.system'):
    if f'"{lib}"' not in bp: errors.append(f'missing test stub library in Android.bp: {lib}')


# System integration sanity checks.
manifest_tree=ET.parse(root/'AndroidManifest.xml').getroot()
app_node=manifest_tree.find('application')
viewer=None
if app_node is not None:
    for a in app_node.findall('activity'):
        if a.attrib.get(ns+'name')=='.viewer.ViewerActivity': viewer=a; break
if viewer is None: errors.append('ViewerActivity missing from manifest')
else:
    if viewer.attrib.get(ns+'exported')!='true': errors.append('ViewerActivity must be exported for ACTION_VIEW system integration')
    view_types=set()
    for f in viewer.findall('intent-filter'):
        actions={x.attrib.get(ns+'name') for x in f.findall('action')}
        if 'android.intent.action.VIEW' in actions:
            view_types|={x.attrib.get(ns+'mimeType') for x in f.findall('data') if x.attrib.get(ns+'mimeType')}
    for required_type in ('image/*','video/*'):
        if required_type not in view_types: errors.append(f'ViewerActivity missing ACTION_VIEW {required_type}')

if 'privileged: true' in bp: errors.append('Gallery must not be installed as privileged without privileged permission need')
if 'LemenzoGalleryDefaultPermissions' in bp: errors.append('redundant default-permissions prebuilt must not be bundled for SYSTEM_GALLERY')
if (root/'system/default-permissions-com.lemenzo.gallery.xml').exists(): errors.append('redundant default-permissions XML present; SYSTEM_GALLERY role must be permission source of truth')
if not (root/'integration/lemenzo_gallery_product.mk').exists(): errors.append('product integration makefile snippet missing')

manifest=(root/'AndroidManifest.xml').read_text()
for perm in ('android.permission.READ_MEDIA_IMAGES','android.permission.READ_MEDIA_VIDEO','android.permission.READ_MEDIA_VISUAL_USER_SELECTED','android.permission.ACCESS_MEDIA_LOCATION'):
    if perm not in manifest: errors.append(f'manifest missing {perm}')
if 'android.permission.INTERNET' in manifest: errors.append('INTERNET permission present')
if 'android.permission.MANAGE_EXTERNAL_STORAGE' in manifest: errors.append('MANAGE_EXTERNAL_STORAGE permission present')
search_text=(root/'src/com/lemenzo/gallery/search/SearchEngine.java').read_text(errors='ignore').lower()
if 'mvimg' in search_text or 'name.contains("motion")' in search_text: errors.append('Motion Photo filename heuristic present')
legacy_text='\n'.join(p.read_text(errors='ignore') for p in (root/'res').rglob('*.xml') if p.name!='styles.xml')
if re.search(r'@style/SecureLineUI|@color/sl_|@drawable/sl_', legacy_text): errors.append('direct legacy SecureLine resource reference remains outside compatibility aliases')

def java_code_only(text):
    out=[]; i=0; n=len(text); state='code'
    while i<n:
        c=text[i]; d=text[i+1] if i+1<n else ''
        if state=='code':
            if c=='/' and d=='/': state='line'; out.extend('  '); i+=2; continue
            if c=='/' and d=='*': state='block'; out.extend('  '); i+=2; continue
            if c=='"': state='string'; out.append(' '); i+=1; continue
            if c=="'": state='char'; out.append(' '); i+=1; continue
            out.append(c); i+=1; continue
        if state=='line':
            if c=='\n': state='code'; out.append('\n')
            else: out.append(' ')
            i+=1; continue
        if state=='block':
            if c=='*' and d=='/': state='code'; out.extend('  '); i+=2
            else: out.append('\n' if c=='\n' else ' '); i+=1
            continue
        quote='"' if state=='string' else "'"
        if c=='\\': out.extend('  '); i+=2; continue
        if c==quote: state='code'
        out.append(' '); i+=1
    return ''.join(out)

for jp in list((root/'src').rglob('*.java')) + list((root/'tests/android/src').rglob('*.java')):
    code=java_code_only(jp.read_text(errors='ignore'))
    for op,cl,name in [('{','}','braces'),('(',')','parentheses'),('[',']','brackets')]:
        depth=0
        for ch in code:
            if ch==op: depth+=1
            elif ch==cl:
                depth-=1
                if depth<0: break
        if depth!=0: errors.append(f'unbalanced Java {name}: {jp.relative_to(root)}')


# Premium About screen invariants.
about_java = root/'src/com/lemenzo/gallery/AboutActivity.java'
about_layout = root/'res/layout/activity_about_gallery.xml'
if not about_java.exists(): errors.append('AboutActivity missing')
if not about_layout.exists(): errors.append('premium About layout missing')
if about_layout.exists():
    about_text = about_layout.read_text(errors='ignore')
    for required in ('@drawable/lemenzo_gallery_logo','@string/about_privacy_title','@string/about_features_title','@string/about_ecosystem_title','@string/about_version'):
        if required not in about_text: errors.append(f'About layout missing {required}')
manifest_about = None
if app_node is not None:
    for a in app_node.findall('activity'):
        if a.attrib.get(ns+'name')=='.AboutActivity': manifest_about=a; break
if manifest_about is None: errors.append('AboutActivity missing from manifest')
elif manifest_about.attrib.get(ns+'exported')!='false': errors.append('AboutActivity must remain internal/exported=false')
main_src=(root/'src/com/lemenzo/gallery/MainActivity.java').read_text(errors='ignore')
if 'AboutActivity.class' not in main_src or 'R.id.more_button' not in main_src: errors.append('MainActivity does not expose About through overflow menu')
if 'R.id.trash_button' in main_src: errors.append('stale direct trash_button reference remains after overflow integration')
for required_string in ('about_description_primary','about_description_secondary','about_privacy_telemetry','about_privacy_analytics','about_privacy_cloud','about_privacy_account','about_privacy_local','about_feature_library','about_feature_formats','about_feature_editing','about_feature_private_share','about_feature_smart','about_feature_camera_files','about_feature_vault','about_ecosystem_body','about_version'):
    if required_string not in strings: errors.append(f'About string missing: {required_string}')


# Locale parity and formatting-placeholder safety. English values/ is canonical.
def _string_map(path):
    return {e.attrib['name']: ''.join(e.itertext()) for e in ET.parse(path).getroot() if e.tag == 'string'}
base_strings_map = _string_map(root/'res/values/strings.xml')
placeholder_re = re.compile(r'%(?:\d+\$)?[a-zA-Z]')
for locale in ('da','sv','de','es'):
    lp = root/f'res/values-{locale}/strings.xml'
    if not lp.exists():
        errors.append(f'missing locale strings: values-{locale}/strings.xml')
        continue
    lm = _string_map(lp)
    if set(lm) != set(base_strings_map):
        missing = sorted(set(base_strings_map)-set(lm))
        extra = sorted(set(lm)-set(base_strings_map))
        errors.append(f'locale values-{locale} key mismatch missing={missing} extra={extra}')
    for key, english in base_strings_map.items():
        if key not in lm: continue
        if sorted(placeholder_re.findall(english)) != sorted(placeholder_re.findall(lm[key])):
            errors.append(f'locale values-{locale} placeholder mismatch: {key}')



# v2 performance + premium ecosystem invariants.
media_ds = (root/'src/com/lemenzo/gallery/data/MediaStoreDataSource.java').read_text(errors='ignore')
if 'loadPageAfter' not in media_ds or 'loadFilesPageAfter' not in media_ds:
    errors.append('large-library seek/keyset paging missing')
if 'INFERRED_DATE).append("<? OR ("' not in media_ds:
    errors.append('seek paging must page by inferred date/id rather than growing OFFSET')
thumb_src = (root/'src/com/lemenzo/gallery/loader/ThumbnailLoader.java').read_text(errors='ignore')
for required in ('ArrayBlockingQueue','MAX_QUEUED_REQUESTS','THREAD_PRIORITY_BACKGROUND','DiscardOldestPolicy'):
    if required not in thumb_src: errors.append(f'thumbnail performance guard missing: {required}')
cache_src = (root/'src/com/lemenzo/gallery/loader/BitmapMemoryCache.java').read_text(errors='ignore')
if 'MAX_CACHE_KB = 48 * 1024' not in cache_src: errors.append('thumbnail memory cache must remain capped')
observer_src = (root/'src/com/lemenzo/gallery/data/MediaObserver.java').read_text(errors='ignore')
if 'DEBOUNCE_MS' not in observer_src or 'postDelayed' not in observer_src:
    errors.append('MediaStore observer burst debounce missing')
insets_src = (root/'src/com/lemenzo/gallery/ui/SystemInsets.java').read_text(errors='ignore')
for required in ('WindowCompat.setDecorFitsSystemWindows','WindowInsets.Type.statusBars()','WindowInsets.Type.navigationBars()','WindowInsets.Type.displayCutout()'):
    if required not in insets_src: errors.append(f'safe inset handling missing: {required}')
about_src = (root/'src/com/lemenzo/gallery/AboutActivity.java').read_text(errors='ignore')
if 'SystemInsets.apply(this)' not in about_src: errors.append('AboutActivity must start below status bar/display cutout')
main_layout_text = (root/'res/layout/activity_main.xml').read_text(errors='ignore')
if 'fitsSystemWindows="true"' in main_layout_text or 'paddingTop="40dp"' in main_layout_text:
    errors.append('Main gallery must not use hard-coded system-bar padding')
if '@drawable/bg_tabs_container' not in main_layout_text or '@drawable/bg_tab_selected' not in main_layout_text:
    errors.append('premium Lemenzo tab surface missing')
colors_text = (root/'res/values/colors.xml').read_text(errors='ignore').upper()
for color in ('#FF08090C','#FF0A1019','#FF101722','#FF3F8CFF','#FFB7C8E6'):
    if color not in colors_text: errors.append(f'LemenzoOS palette color missing: {color}')
for artifact in root.rglob('*'):
    if artifact.is_file() and (artifact.suffix in ('.pyc','.pyo','.orig','.rej','.tmp') or '.backup' in artifact.name):
        errors.append(f'temporary/generated source artifact present: {artifact.relative_to(root)}')

print(f'java={len(list((root/"src").rglob("*.java")))} xml={len(list((root/"res").rglob("*.xml")))} strings={len(strings)} ids={len(ids)}')

# v1.20 large-library responsiveness / build-safety gates
media_ds = (root/'src/com/lemenzo/gallery/data/MediaStoreDataSource.java').read_text(errors='ignore')
if 'return loadFilesPage(mode, offset, limit, bucketId);' in media_ds:
    errors.append('loadAlbumPage must not reference an undefined mode variable')
if 'return loadFilesPage(MediaQuery.ALL, offset, limit, bucketId);' not in media_ds:
    errors.append('legacy album offset compatibility path must compile with MediaQuery.ALL')
background = (root/'src/com/lemenzo/gallery/util/BackgroundExecutors.java').read_text(errors='ignore')
for required in ('THREAD_PRIORITY_BACKGROUND','newSingleThreadExecutor'):
    if required not in background: errors.append('large-library executors must run at Android background priority')
albums_fragment = (root/'src/com/lemenzo/gallery/albums/AlbumsFragment.java').read_text(errors='ignore')
if 'loadTask.cancel(true)' not in albums_fragment or 'loadGeneration' not in albums_fragment:
    errors.append('album summary refresh must cancel stale full-library scans')
search_activity = (root/'src/com/lemenzo/gallery/search/SearchActivity.java').read_text(errors='ignore')
if 'searchTask.cancel(true)' not in search_activity:
    errors.append('search must cancel stale scans when the query changes')
search_vm = (root/'src/com/lemenzo/gallery/search/SearchViewModel.java').read_text(errors='ignore')
if 'if(blank)return new Result(results,false);' not in search_vm:
    errors.append('blank search must not scan MediaStore')


# v1.21 editor UX/performance gates
editor_layout=(root/'res/layout/activity_editor.xml').read_text(errors='ignore')
for required in ('editor_tab_adjust','editor_tab_crop','editor_tab_filters','editor_tab_markup','bg_editor_sheet'):
    if required not in editor_layout: errors.append(f'modern photo editor structure missing: {required}')
editor_src=(root/'src/com/lemenzo/gallery/editor/EditorActivity.java').read_text(errors='ignore')
for required in ('PREVIEW_MAX_EDGE = 1600','PREVIEW_DEBOUNCE_MS','previewExecutor','previewGeneration','BackgroundExecutors.single("LemenzoGalleryEditorPreview")'):
    if required not in editor_src: errors.append(f'photo editor performance guard missing: {required}')
if 'private void renderPreview(){if(basePreviewBitmap==null)return;Bitmap rendered=null' in editor_src:
    errors.append('photo preview rendering must not run synchronously on UI thread')
video_layout=(root/'res/layout/activity_video_editor.xml').read_text(errors='ignore')
for required in ('video_play_pause','bg_editor_sheet','video_editor_hint'):
    if required not in video_layout: errors.append(f'modern video editor structure missing: {required}')
video_src=(root/'src/com/lemenzo/gallery/video/VideoEditorActivity.java').read_text(errors='ignore')
if 'BackgroundExecutors.single("LemenzoGalleryVideoEditor")' not in video_src: errors.append('video metadata/export worker must use background-priority executor')
if 'private void readDuration() {\n        MediaMetadataRetriever' in video_src: errors.append('video duration metadata must not be read synchronously on UI thread')

if errors:
    print('\n'.join(errors)); sys.exit(1)
print('STATIC VALIDATION: PASS')

# Locale configuration / per-app language integration
locale_config = root / 'res/xml/locale_config.xml'
assert locale_config.exists(), 'Missing res/xml/locale_config.xml'
locale_xml = locale_config.read_text()
for locale in ('en','da','sv','de','es'):
    assert f'android:name="{locale}"' in locale_xml, f'locale_config missing {locale}'
manifest_text = (root / 'AndroidManifest.xml').read_text()
assert 'android:localeConfig="@xml/locale_config"' in manifest_text, 'Manifest missing android:localeConfig'
