#!/usr/bin/env python3
"""Operator-only Photoshop JSX runner CLI POC.

IMPORTANT SAFETY BOUNDARY
- `doctor` never launches Photoshop.
- `run` launches/controls Photoshop via AppleScript/Apple Events and therefore is
  GUI/desktop automation under the current bot1 profile policy.
- `run` is guarded so non-visual-operator profiles cannot accidentally use it.

The point of this POC is to convert the manual Photoshop menu/file-picking step
into one deterministic command for the authorized GUI operator or an
operator-owned service.
"""
from __future__ import annotations

import argparse
import json
import os
import plistlib
import shlex
import subprocess
import sys
import time
from pathlib import Path
from typing import Any

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


def current_profile_hint() -> str:
    keys = ['HERMES_PROFILE', 'HERMES_ACTIVE_PROFILE', 'HERMES_PROFILE_NAME']
    vals = [os.environ.get(k, '') for k in keys]
    home = os.environ.get('HERMES_HOME', '')
    vals.append(home)
    return ' '.join(v for v in vals if v)


def assert_operator_allowed(args: argparse.Namespace) -> None:
    hint = current_profile_hint().lower()
    if 'visual-operator' in hint:
        return
    if args.operator_ack != 'visual-operator':
        raise SystemExit(
            'REFUSED: this command would launch/control Photoshop via AppleScript/Apple Events.\n'
            'Current policy allows only the visual-operator profile to run it.\n'
            'Run doctor instead, or invoke from visual-operator with: --operator-ack visual-operator'
        )


def system_probe(app: Path = DEFAULT_APP) -> dict[str, Any]:
    info = app / 'Contents' / 'Info.plist'
    sdef = app / 'Contents' / 'Resources' / 'Photoshop.sdef'
    result: dict[str, Any] = {
        'app_path': str(app),
        'app_exists': app.exists(),
        'info_plist_exists': info.exists(),
        'sdef_exists': sdef.exists(),
        'bundle_identifier': None,
        'bundle_executable': None,
        'apple_script_enabled': None,
        'sdef_has_javascript_file_parameter': False,
        'technical_feasibility': False,
        'launches_photoshop': False,
    }
    if info.exists():
        plist = plistlib.load(info.open('rb'))
        result['bundle_identifier'] = plist.get('CFBundleIdentifier')
        result['bundle_executable'] = plist.get('CFBundleExecutable')
        result['apple_script_enabled'] = plist.get('NSAppleScriptEnabled')
    if sdef.exists():
        text = sdef.read_text(encoding='utf-8', errors='replace')
        result['sdef_has_javascript_file_parameter'] = 'JavaScript/File' in text or 'JavaScript file to execute' in text
    result['technical_feasibility'] = bool(
        result['app_exists']
        and result['info_plist_exists']
        and result['sdef_exists']
        and result['apple_script_enabled'] in ('Yes', True, 'true', '1')
        and result['sdef_has_javascript_file_parameter']
    )
    return result


def doctor(args: argparse.Namespace) -> int:
    work = Path(args.work_dir).expanduser().resolve() if args.work_dir else None
    app_probe = system_probe(Path(args.app).expanduser())
    result: dict[str, Any] = {
        'mode': 'doctor_no_launch',
        'launches_photoshop': False,
        'uses_applescript': False,
        'system': app_probe,
        'errors': [],
        'warnings': [],
    }
    if work:
        jsx = Path(args.jsx)
        if not jsx.is_absolute():
            jsx = work / jsx
        base = Path(args.base)
        if not base.is_absolute():
            base = work / base
        out = Path(args.out)
        if not out.is_absolute():
            out = work / out
        log = Path(args.log)
        if not log.is_absolute():
            log = work / log
        result['handoff'] = {
            'work_dir': str(work),
            'work_dir_exists': work.exists(),
            'jsx': str(jsx),
            'jsx_exists': jsx.exists(),
            'base': str(base),
            'base_exists': base.exists(),
            'out': str(out),
            'out_exists': out.exists(),
            'log': str(log),
            'log_exists': log.exists(),
        }
        if not work.exists():
            result['errors'].append('work_dir missing')
        if not jsx.exists():
            result['errors'].append('jsx missing')
        if not base.exists():
            result['errors'].append('base PSD missing')
        if out.exists() and not args.allow_overwrite:
            result['warnings'].append('output PSD already exists; run requires --allow-overwrite')
    if not app_probe['technical_feasibility']:
        result['errors'].append('Photoshop scripting feasibility probe failed')
    print(json.dumps(result, ensure_ascii=False, indent=2))
    return 0 if not result['errors'] else 2


def apple_script_for_jsx(jsx: Path) -> str:
    # Photoshop.sdef declares the JavaScript "File" parameter as type text.
    # Passing an AppleScript alias caused Photoshop 2026 to fail with -1700
    # (cannot convert data to expected type). Pass the POSIX path string.
    posix = str(jsx).replace('\\', '\\\\').replace('"', '\\"')
    return f'''
tell application id "{BUNDLE_ID}"
    activate
    do javascript file "{posix}"
end tell
'''.strip()


def run(args: argparse.Namespace) -> int:
    assert_operator_allowed(args)
    work = Path(args.work_dir).expanduser().resolve()
    jsx = Path(args.jsx)
    if not jsx.is_absolute():
        jsx = work / jsx
    base = Path(args.base)
    if not base.is_absolute():
        base = work / base
    out = Path(args.out)
    if not out.is_absolute():
        out = work / out
    log = Path(args.log)
    if not log.is_absolute():
        log = work / log

    pre_errors = []
    if not work.exists():
        pre_errors.append(f'work_dir missing: {work}')
    if not jsx.exists():
        pre_errors.append(f'jsx missing: {jsx}')
    if not base.exists():
        pre_errors.append(f'base PSD missing: {base}')
    if out.exists() and not args.allow_overwrite:
        pre_errors.append(f'output exists; pass --allow-overwrite if intentional: {out}')
    if pre_errors:
        print(json.dumps({'status': 'preflight_failed', 'errors': pre_errors}, ensure_ascii=False, indent=2))
        return 2

    script = apple_script_for_jsx(jsx)
    started = time.time()
    proc = subprocess.run(
        ['/usr/bin/osascript', '-e', script],
        text=True,
        capture_output=True,
        timeout=args.timeout,
    )
    elapsed = round(time.time() - started, 3)
    log_text = log.read_text(encoding='utf-8', errors='replace') if log.exists() else ''
    markers = {
        'PATCH_VERSION': 'PATCH_VERSION' in log_text,
        'OPENED BASE PSD': 'OPENED BASE PSD' in log_text,
        'SAVED PSD': 'SAVED PSD' in log_text,
        'TEXT': 'TEXT' in log_text,
    }
    result = {
        'status': 'success' if proc.returncode == 0 and out.exists() and markers['SAVED PSD'] else 'failed_or_incomplete',
        'elapsed_seconds': elapsed,
        'returncode': proc.returncode,
        'stdout': proc.stdout[-4000:],
        'stderr': proc.stderr[-4000:],
        'work_dir': str(work),
        'jsx': str(jsx),
        'base': str(base),
        'out': {'path': str(out), 'exists': out.exists(), 'size_bytes': out.stat().st_size if out.exists() else 0},
        'log': {'path': str(log), 'exists': log.exists(), 'size_bytes': log.stat().st_size if log.exists() else 0},
        'log_markers': markers,
    }
    print(json.dumps(result, ensure_ascii=False, indent=2))
    return 0 if result['status'] == 'success' else 1


def main() -> int:
    p = argparse.ArgumentParser(description='Photoshop PSD finalize CLI POC')
    sub = p.add_subparsers(dest='cmd', required=True)

    common = argparse.ArgumentParser(add_help=False)
    common.add_argument('--app', default=str(DEFAULT_APP))
    common.add_argument('--work-dir')
    common.add_argument('--jsx', default='build_editable_text_overlay.jsx')
    common.add_argument('--base', default='layered_raster_base_no_text.psd')
    common.add_argument('--out', default='final_photoshop_editable_text.psd')
    common.add_argument('--log', default='photoshop_build_log.txt')
    common.add_argument('--allow-overwrite', action='store_true')

    d = sub.add_parser('doctor', parents=[common], help='No-launch preflight check')
    d.set_defaults(func=doctor)

    r = sub.add_parser('run', parents=[common], help='Launch/control Photoshop to run JSX; operator-only')
    r.add_argument('--timeout', type=int, default=180)
    r.add_argument('--operator-ack', default='', help='Must be visual-operator unless env identifies visual-operator')
    r.set_defaults(func=run)

    args = p.parse_args()
    return args.func(args)


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