#!/usr/bin/env python3
"""Dry-run checker for Photoshop JSX PSD finalize handoff packages.

This script intentionally DOES NOT launch Photoshop and DOES NOT use AppleScript.
It statically checks whether a Photoshop install exposes scripting support and
whether a handoff folder looks portable/safe enough for a future CLI runner.
"""
from __future__ import annotations

import argparse
import json
import os
import plistlib
import re
import sys
from pathlib import Path
from typing import Any

DEFAULT_APP = Path('/Applications/Adobe Photoshop 2026/Adobe Photoshop 2026.app')


def plist_value(plist: dict[str, Any], key: str) -> Any:
    return plist.get(key, None)


def system_probe(app: Path = DEFAULT_APP) -> dict[str, Any]:
    info_path = app / 'Contents' / 'Info.plist'
    sdef_path = app / 'Contents' / 'Resources' / 'Photoshop.sdef'
    exe_dir = app / 'Contents' / 'MacOS'

    result: dict[str, Any] = {
        'mode': 'system_probe_no_launch',
        'launches_photoshop': False,
        'uses_applescript': False,
        'app_path': str(app),
        'app_exists': app.exists(),
        'info_plist': str(info_path),
        'info_plist_exists': info_path.exists(),
        'sdef_path': str(sdef_path),
        'sdef_exists': sdef_path.exists(),
        'errors': [],
        'warnings': [],
        'evidence': {},
    }

    if info_path.exists():
        try:
            with info_path.open('rb') as f:
                plist = plistlib.load(f)
            result['bundle_identifier'] = plist_value(plist, 'CFBundleIdentifier')
            result['bundle_executable'] = plist_value(plist, 'CFBundleExecutable')
            result['apple_script_enabled'] = plist_value(plist, 'NSAppleScriptEnabled')
            result['osascripting_definition'] = plist_value(plist, 'OSAScriptingDefinition')
            exe = exe_dir / str(result.get('bundle_executable') or '')
            result['bundle_executable_path'] = str(exe)
            result['bundle_executable_exists'] = exe.exists()
        except Exception as e:  # pragma: no cover - diagnostic output
            result['errors'].append(f'failed_to_read_info_plist: {e}')
    else:
        result['errors'].append('Photoshop Info.plist not found')

    if sdef_path.exists():
        try:
            text = sdef_path.read_text(encoding='utf-8', errors='replace')
            result['evidence']['sdef_has_adobe_script_automation_suite'] = 'AdobeScriptAutomation' in text
            result['evidence']['sdef_has_javascript_file_parameter'] = 'JavaScript/File' in text or 'JavaScript file to execute' in text
            result['evidence']['sdef_has_javascript_text_parameter'] = 'JavaScript/Text' in text or 'JavaScript text to execute' in text
            result['evidence']['sdef_script_command_excerpt'] = '\n'.join(text.splitlines()[3:12])
        except Exception as e:  # pragma: no cover
            result['errors'].append(f'failed_to_read_sdef: {e}')
    else:
        result['errors'].append('Photoshop.sdef not found')

    if not result['app_exists']:
        result['warnings'].append('Photoshop app path not found; pass --app if installed elsewhere')
    if result.get('apple_script_enabled') not in ('Yes', True, 'true', '1'):
        result['warnings'].append('NSAppleScriptEnabled is not clearly enabled')
    if not result['evidence'].get('sdef_has_javascript_file_parameter'):
        result['warnings'].append('sdef JavaScript file execution parameter not found')

    result['technical_feasibility'] = bool(
        result['app_exists']
        and result['info_plist_exists']
        and result['sdef_exists']
        and result.get('apple_script_enabled') in ('Yes', True, 'true', '1')
        and result['evidence'].get('sdef_has_javascript_file_parameter')
    )
    result['boundary'] = (
        'A CLI wrapper is feasible as an Apple Events/Photoshop scripting runner, '
        'but actual execution would launch/control Photoshop and is not pure headless CLI.'
    )
    return result


def relpath_or_abs(base: Path, value: str) -> Path:
    p = Path(value)
    return p if p.is_absolute() else base / p


def scan_jsx(jsx_path: Path, work_dir: Path) -> dict[str, Any]:
    result: dict[str, Any] = {
        'path': str(jsx_path),
        'exists': jsx_path.exists(),
        'errors': [],
        'warnings': [],
        'findings': {},
    }
    if not jsx_path.exists():
        result['errors'].append('JSX file missing')
        return result

    text = jsx_path.read_text(encoding='utf-8', errors='replace')
    result['size_bytes'] = jsx_path.stat().st_size
    result['findings']['uses_script_location'] = '$.fileName' in text or 'File($.fileName)' in text
    result['findings']['mentions_PATCH_VERSION'] = 'PATCH_VERSION' in text
    result['findings']['mentions_OPENED_BASE_PSD'] = 'OPENED BASE PSD' in text
    result['findings']['mentions_SAVED_PSD'] = 'SAVED PSD' in text
    result['findings']['has_alert'] = bool(re.search(r'\balert\s*\(', text))
    result['findings']['has_bring_to_front'] = 'bringToFront' in text
    result['findings']['has_abs_hermes_profile_path'] = '/.hermes/profiles/' in text or '/Users/bot1/.hermes/profiles/' in text
    result['findings']['has_legacy_ai_work_path'] = '/Users/bot1/AI Work' in text
    result['findings']['has_nas_user_material_path'] = '/Users/bot1/Volumes/root_for_ai/03_用户资料' in text
    result['findings']['sets_display_dialogs'] = 'displayDialogs' in text or 'display dialogs' in text

    if not result['findings']['uses_script_location']:
        result['warnings'].append('JSX may not derive WORK_DIR from $.fileName; portability risk')
    if result['findings']['has_alert']:
        result['warnings'].append('JSX contains alert(); unattended CLI/operator execution may block')
    if result['findings']['has_bring_to_front']:
        result['warnings'].append('JSX contains app.bringToFront(); unnecessary GUI focus side effect')
    if result['findings']['has_abs_hermes_profile_path']:
        result['warnings'].append('JSX contains Hermes profile absolute path; not portable')
    if result['findings']['has_legacy_ai_work_path']:
        result['warnings'].append('JSX contains legacy /Users/bot1/AI Work path')
    if result['findings']['has_nas_user_material_path']:
        result['warnings'].append('JSX references 03_用户资料; must remain read-only source material')
    for marker in ['mentions_PATCH_VERSION', 'mentions_OPENED_BASE_PSD', 'mentions_SAVED_PSD']:
        if not result['findings'][marker]:
            result['warnings'].append(f'JSX missing log marker: {marker}')
    return result


def check_manifest(manifest_path: Path, work_dir: Path) -> dict[str, Any]:
    result: dict[str, Any] = {
        'path': str(manifest_path),
        'exists': manifest_path.exists(),
        'errors': [],
        'warnings': [],
        'layer_count': None,
        'missing_layer_files': [],
        'absolute_layer_paths': [],
    }
    if not manifest_path.exists():
        result['warnings'].append('manifest not found; pass --manifest if located elsewhere')
        return result
    try:
        data = json.loads(manifest_path.read_text(encoding='utf-8'))
    except Exception as e:
        result['errors'].append(f'failed_to_parse_manifest_json: {e}')
        return result

    layers = data.get('layers') if isinstance(data, dict) else None
    if not isinstance(layers, list):
        result['errors'].append('manifest JSON does not contain a list field named layers')
        return result
    result['layer_count'] = len(layers)
    for i, layer in enumerate(layers):
        if not isinstance(layer, dict):
            result['warnings'].append(f'layer[{i}] is not an object')
            continue
        f = layer.get('file_local') or layer.get('file') or layer.get('path')
        if not f:
            continue
        p = Path(str(f))
        if p.is_absolute():
            result['absolute_layer_paths'].append(str(p))
        actual = p if p.is_absolute() else manifest_path.parent / p
        if not actual.exists():
            # Also try relative to work_dir for manifests that store project-relative paths.
            actual2 = work_dir / p
            if not actual2.exists():
                result['missing_layer_files'].append(str(f))
    if result['absolute_layer_paths']:
        result['warnings'].append(f'absolute layer paths found: {len(result["absolute_layer_paths"])}')
    if result['missing_layer_files']:
        result['errors'].append(f'missing referenced layer files: {len(result["missing_layer_files"])}')
    return result


def handoff_probe(args: argparse.Namespace) -> dict[str, Any]:
    work_dir = Path(args.work_dir).expanduser().resolve()
    jsx = relpath_or_abs(work_dir, args.jsx)
    base = relpath_or_abs(work_dir, args.base)
    manifest = relpath_or_abs(work_dir, args.manifest) if args.manifest else work_dir / 'psd_export_work' / 'layer_manifest.json'
    out = relpath_or_abs(work_dir, args.out)
    log = relpath_or_abs(work_dir, args.log)

    result: dict[str, Any] = {
        'mode': 'handoff_probe_no_launch',
        'launches_photoshop': False,
        'uses_applescript': False,
        'work_dir': str(work_dir),
        'work_dir_exists': work_dir.exists(),
        'base_psd': {'path': str(base), 'exists': base.exists(), 'size_bytes': base.stat().st_size if base.exists() else 0},
        'expected_output_psd': {'path': str(out), 'exists': out.exists(), 'size_bytes': out.stat().st_size if out.exists() else 0},
        'expected_log': {'path': str(log), 'exists': log.exists(), 'size_bytes': log.stat().st_size if log.exists() else 0},
        'jsx': scan_jsx(jsx, work_dir),
        'manifest': check_manifest(manifest, work_dir),
        'errors': [],
        'warnings': [],
    }
    if not work_dir.exists():
        result['errors'].append('work_dir missing')
    if not base.exists():
        result['errors'].append('base PSD missing')
    if result['expected_output_psd']['exists']:
        result['warnings'].append('expected output PSD already exists; actual runner must avoid accidental overwrite unless explicit')

    result['errors'].extend(result['jsx']['errors'])
    result['warnings'].extend(result['jsx']['warnings'])
    result['errors'].extend(result['manifest']['errors'])
    result['warnings'].extend(result['manifest']['warnings'])
    result['ready_for_operator_cli_runner'] = len(result['errors']) == 0
    return result


def main() -> int:
    parser = argparse.ArgumentParser(description='Dry-run doctor for Photoshop JSX PSD finalize packages')
    parser.add_argument('--app', default=str(DEFAULT_APP), help='Photoshop .app path')
    parser.add_argument('--system-only', action='store_true', help='Only probe Photoshop install statically')
    parser.add_argument('--work-dir', help='PSD handoff work directory')
    parser.add_argument('--jsx', default='build_editable_text_overlay.jsx')
    parser.add_argument('--base', default='layered_raster_base_no_text.psd')
    parser.add_argument('--manifest', default='psd_export_work/layer_manifest.json')
    parser.add_argument('--out', default='final_photoshop_editable_text.psd')
    parser.add_argument('--log', default='photoshop_build_log.txt')
    parser.add_argument('--json', action='store_true', help='Print JSON')
    args = parser.parse_args()

    app_probe = system_probe(Path(args.app).expanduser())
    if args.system_only or not args.work_dir:
        result = app_probe
    else:
        result = {'system': app_probe, 'handoff': handoff_probe(args)}
        result['overall_ready_for_cli_runner'] = bool(app_probe['technical_feasibility'] and result['handoff']['ready_for_operator_cli_runner'])

    if args.json:
        print(json.dumps(result, ensure_ascii=False, indent=2))
    else:
        print('PSD finalize doctor')
        print(json.dumps(result, ensure_ascii=False, indent=2))
    return 0 if (result.get('technical_feasibility') or result.get('overall_ready_for_cli_runner')) else 2


if __name__ == '__main__':
    raise SystemExit(main())
