<?php
/** IPMI terminal editor v1.0. CLI only. No stored credentials. */
declare(strict_types=1);
if (PHP_SAPI !== 'cli') { http_response_code(404); exit; }
umask(0077);
try {
    $site = realpath(getenv('IPMI_SITE') ?: '/home/ipmievents/intercessoryprayerministry.org/new-site');
    if (!$site || !is_file($site . '/configuration.php')) throw new RuntimeException('Set IPMI_SITE to the Joomla directory.');
    $username = getenv('IPMI_EDITOR');
    if (!$username) throw new RuntimeException('Set IPMI_EDITOR to your authorized Joomla username.');
    $_SERVER['HTTP_HOST'] = $_SERVER['SERVER_NAME'] = 'intercessoryprayerministry.org';
    $_SERVER['HTTPS'] = 'on'; $_SERVER['SERVER_PORT'] = 443;
    $_SERVER['SCRIPT_NAME'] = $_SERVER['PHP_SELF'] = $_SERVER['REQUEST_URI'] = '/new-site/administrator/index.php';
    define('_JEXEC', 1); define('JPATH_BASE', $site);
    require $site . '/includes/defines.php'; require $site . '/includes/framework.php';
    $container = \Joomla\CMS\Factory::getContainer();
    $container->alias('session', 'session.cli')->alias(\Joomla\CMS\Session\Session::class, 'session.cli')->alias(\Joomla\Session\Session::class, 'session.cli')->alias(\Joomla\Session\SessionInterface::class, 'session.cli');
    $app = $container->get(\Joomla\CMS\Application\AdministratorApplication::class);
    \Joomla\CMS\Factory::$application = $app;
    $db = $container->get(\Joomla\Database\DatabaseInterface::class);
    $uid = (int) $db->setQuery('SELECT id FROM #__users WHERE block=0 AND username=' . $db->quote($username))->loadResult();
    if (!$uid) throw new RuntimeException('Authorized Joomla account not found or blocked.');
    $user = $container->get(\Joomla\CMS\User\UserFactoryInterface::class)->loadUserById($uid);
    if (!$user->authorise('core.login.admin')) throw new RuntimeException('Account lacks administrator login permission.');
    $app->loadIdentity($user);
    $app->loadLanguage($container->get(\Joomla\CMS\Language\LanguageFactoryInterface::class)->createLanguage('en-GB', false));
    \Joomla\CMS\Factory::$language = $app->getLanguage();
    $app->getLanguage()->load('joomla', JPATH_ADMINISTRATOR);
    $app->createExtensionNamespaceMap();
    // SP Page Builder's content plugin expects this class during CLI article saves.
    require_once JPATH_SITE . '/components/com_sppagebuilder/helpers/autoload.php';
    $cmd = $argv[1] ?? 'help';
    $backupDir = __DIR__ . '/backups';
    $json = static fn($v) => json_encode($v, JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
    $read = static function ($file) { if (!$file || !is_file($file)) throw new RuntimeException('JSON file not found.'); return json_decode(file_get_contents($file), true, 512, JSON_THROW_ON_ERROR); };
    $hash = static function (array $r) { unset($r['hits'], $r['checked_out'], $r['checked_out_time']); return hash('sha256', json_encode($r, JSON_THROW_ON_ERROR)); };
    $backup = static function (string $kind, array $r) use ($backupDir, $json): string {
        if (!is_dir($backupDir) && !mkdir($backupDir, 0700, true)) throw new RuntimeException('Cannot create private backup folder.');
        $f = $backupDir . '/' . $kind . '-' . ($r['id'] ?? 0) . '-' . gmdate('Ymd-His') . '-' . bin2hex(random_bytes(3)) . '.json';
        if (file_put_contents($f, $json($r), LOCK_EX) === false) throw new RuntimeException('Backup failed.');
        return $f;
    };
    $get = static function (string $table, int $id) use ($db): array {
        if ($id < 1) throw new RuntimeException('A positive ID is required.');
        $r = $db->setQuery('SELECT * FROM #__' . $table . ' WHERE id=' . $id)->loadAssoc();
        if (!$r) throw new RuntimeException('Record not found.'); return $r;
    };
    $guard = static function (array $r, array $input) use ($hash): void {
        if (!empty($r['checked_out'])) throw new RuntimeException('Record checked out. Ask the editor to Save & Close or Cancel; do not force a check-in.');
        if (!isset($input['source_hash']) || !hash_equals($hash($r), $input['source_hash'])) throw new RuntimeException('Record changed since export. Export again and merge your edits.');
    };
    $cleanCache = static function () use ($container): void {
        $factory = $container->get(\Joomla\CMS\Cache\CacheControllerFactoryInterface::class);
        foreach (['com_content','com_sppagebuilder','mod_ipmi_content','page'] as $group) $factory->createCacheController('callback', ['defaultgroup'=>$group])->clean();
    };
    if ($cmd === 'inventory') {
        $out = ['version'=>JVERSION, 'site'=>$site];
        foreach (['categories'=>"SELECT id,title,alias,parent_id,published FROM #__categories WHERE extension='com_content'", 'articles'=>'SELECT id,title,alias,catid,state,featured,publish_up,publish_down,checked_out FROM #__content', 'pages'=>'SELECT id,title,published,checked_out FROM #__sppagebuilder', 'menus'=>"SELECT id,title,alias,link,menutype,parent_id,level FROM #__menu WHERE client_id=0 AND published=1", 'modules'=>"SELECT id,title,published,params FROM #__modules WHERE module='mod_ipmi_content'"] as $k=>$q) $out[$k]=$db->setQuery($q)->loadAssocList();
        echo $json($out) . "\n";
    } elseif ($cmd === 'article-export') {
        $r=$get('content',(int)($argv[2]??0));
        $category=$db->setQuery('SELECT alias FROM #__categories WHERE id='.(int)$r['catid'])->loadResult();
        $out=['id'=>(int)$r['id'],'source_hash'=>$hash($r),'category'=>$category];
        foreach(['title','alias','introtext','fulltext','state','featured','access','language','created_by_alias','publish_up','publish_down','metadesc'] as $key)$out[$key]=$r[$key];
        $out['images']=json_decode($r['images']?:'{}',true);
        $file=$argv[3]??null; if(!$file)throw new RuntimeException('Specify output JSON path.');
        if(file_exists($file))throw new RuntimeException('Output exists; use a new filename.');
        if(file_put_contents($file,$json($out))===false)throw new RuntimeException('Cannot write export.');
        echo $json(['export'=>$file,'id'=>$r['id']])."\n";
    } elseif ($cmd === 'article-save') {
        $input=$read($argv[2]??null); $id=(int)($input['id']??0); $r=$id?$get('content',$id):[];
        if($id){$guard($r,$input);if(!$user->authorise('core.edit','com_content.article.'.$id))throw new RuntimeException('No article edit permission.');}
        $allowed=['ministry-news','reflections','prayer-alerts','prayer-resources'];
        if(!in_array($input['category']??'', $allowed,true))throw new RuntimeException('Use a documented category alias. Daily Devotions uses reflections.');
        $cat=(int)$db->setQuery("SELECT id FROM #__categories WHERE extension='com_content' AND alias=".$db->quote($input['category']))->loadResult();
        if(!$cat)throw new RuntimeException('Category not found.');
        if(!$id&&!$user->authorise('core.create','com_content.category.'.$cat))throw new RuntimeException('No article create permission.');
        $state=(int)($input['state']??($r['state']??0));
        if(!in_array($state,[-2,0,1,2],true))throw new RuntimeException('state must be -2, 0, 1, or 2.');
        if(($state!==0 || ($id&&$state!==(int)$r['state']) || array_key_exists('featured',$input))&&!$user->authorise('core.edit.state','com_content.category.'.$cat))throw new RuntimeException('No publishing permission.');
        foreach(['title','alias'] as $key)if(empty($input[$key]))throw new RuntimeException($key.' is required.');
        if(!preg_match('/^[a-z0-9]+(?:-[a-z0-9]+)*$/',$input['alias']))throw new RuntimeException('Use a lowercase, hyphenated URL alias.');
        if($db->setQuery('SELECT id FROM #__content WHERE catid='.$cat.' AND alias='.$db->quote($input['alias']).' AND id<>'.$id)->loadResult())throw new RuntimeException('Alias already exists. Export and update its ID instead.');
        $data=$r;
        foreach(['title','alias','introtext','fulltext','created_by_alias','metadesc','publish_up','publish_down','access','language','featured'] as $key)if(array_key_exists($key,$input))$data[$key]=$input[$key];
        foreach(['publish_up','publish_down'] as $key)if(!empty($data[$key])){
            $dt=DateTimeImmutable::createFromFormat('!Y-m-d H:i:s',$data[$key],new DateTimeZone('UTC'));
            if(!$dt||$dt->format('Y-m-d H:i:s')!==$data[$key])throw new RuntimeException($key.' must be UTC YYYY-MM-DD HH:MM:SS or null.');
        }
        if(!empty($data['publish_up'])&&!empty($data['publish_down'])&&$data['publish_down']<=$data['publish_up'])throw new RuntimeException('Finish date must follow start date.');
        $data+=['created_by'=>$uid,'created'=>gmdate('Y-m-d H:i:s'),'created_by_alias'=>'IPMI Editorial','access'=>1,'language'=>'*','introtext'=>'','fulltext'=>'','featured'=>0,'attribs'=>'{}','metadata'=>'{}','images'=>'{}'];
        if(isset($input['images'])){
            foreach(['image_intro','image_fulltext'] as $key)if(!empty($input['images'][$key])){
                $path=$input['images'][$key];$resolved=realpath($site.'/'.$path);
                if(!str_starts_with($path,'images/')||!$resolved||!str_starts_with($resolved,$site.'/images/'))throw new RuntimeException('Upload images under Joomla images/ before saving.');
            }
            $data['images']=json_encode($input['images'],JSON_THROW_ON_ERROR);
        }
        $data['id']=$id;$data['catid']=$cat;$data['state']=$state;
        $data['articletext']=$data['introtext'].($data['fulltext']!==''?'<hr id="system-readmore" />'.$data['fulltext']:'');
        $data['publish_up']=$data['publish_up']??($state===1?gmdate('Y-m-d H:i:s'):null);
        $data['publish_down']=$data['publish_down']??null;
        define('JPATH_COMPONENT',JPATH_ADMINISTRATOR.'/components/com_content');
        define('JPATH_COMPONENT_ADMINISTRATOR',JPATH_COMPONENT);define('JPATH_COMPONENT_SITE',JPATH_SITE.'/components/com_content');
        $before=$backup('article',$r?:['id'=>0,'request'=>$input]);
        $model=$app->bootComponent('com_content')->getMVCFactory()->createModel('Article','Administrator',['ignore_request'=>true]);
        if(!$model->save($data))throw new RuntimeException('Joomla save failed: '.$model->getError());
        $saved=$id?: (int)$model->getState('article.id');
        if(!$saved)throw new RuntimeException('Save returned no ID. Inspect inventory before retrying.');
        if(!$model->featured([$saved],(int)$data['featured']))throw new RuntimeException('Article saved but featured update failed; inspect before retrying.');
        $cleanCache();
        echo $json(['saved_id'=>$saved,'state'=>$state,'category'=>$input['category'],'backup'=>$before,'verify'=>'Check inventory, administrator, and public category page.'])."\n";
    } elseif ($cmd === 'page-export') {
        $r=$get('sppagebuilder',(int)($argv[2]??0));
        if(!empty($r['checked_out']))throw new RuntimeException('Page checked out; have the editor close it first.');
        if($r['text']!==$r['content'])throw new RuntimeException('Builder draft and published content differ. Resolve in SP Page Builder before terminal editing.');
        $out=['id'=>(int)$r['id'],'source_hash'=>$hash($r),'title'=>$r['title'],'layout'=>json_decode($r['content'],true,512,JSON_THROW_ON_ERROR)];
        $file=$argv[3]??null;if(!$file||file_exists($file))throw new RuntimeException('Specify a new output filename.');
        if(file_put_contents($file,$json($out))===false)throw new RuntimeException('Cannot write export.');
        echo $json(['export'=>$file,'id'=>$r['id']])."\n";
    } elseif ($cmd === 'page-save') {
        $input=$read($argv[2]??null);$r=$get('sppagebuilder',(int)($input['id']??0));$guard($r,$input);
        if(!$user->authorise('core.edit','com_sppagebuilder'))throw new RuntimeException('No SP Page Builder edit permission.');
        if(!isset($input['layout'])||!is_array($input['layout'])||!array_is_list($input['layout'])||!$input['layout'])throw new RuntimeException('layout must be a nonempty array of existing SP Page Builder sections.');
        foreach($input['layout'] as $section)if(!isset($section['id'],$section['columns'])||!is_array($section['columns']))throw new RuntimeException('Invalid section structure.');
        $before=$backup('page',$r);$layout=json_encode($input['layout'],JSON_THROW_ON_ERROR|JSON_UNESCAPED_SLASHES|JSON_UNESCAPED_UNICODE);
        $db->transactionStart();
        try{
            $locked=$db->setQuery('SELECT * FROM #__sppagebuilder WHERE id='.(int)$r['id'].' FOR UPDATE')->loadAssoc();$guard($locked,$input);
            $o=(object)['id'=>(int)$r['id'],'text'=>$layout,'content'=>$layout,'modified'=>gmdate('Y-m-d H:i:s'),'modified_by'=>$uid];
            $db->updateObject('#__sppagebuilder',$o,'id');$db->transactionCommit();
        }catch(Throwable $e){$db->transactionRollback();throw $e;}
        $cleanCache();echo $json(['saved_id'=>$r['id'],'backup'=>$before,'note'=>'Updated text and content together; title, metadata, status, and menu unchanged.'])."\n";
    } else {
        echo "IPMI terminal editor v1.0\nSet IPMI_SITE and IPMI_EDITOR. Commands:\n  inventory\n  article-export ID new-file.json\n  article-save input.json\n  page-export ID new-file.json\n  page-save input.json\nUpdates require a fresh export/source_hash. New articles default to unpublished.\n";
    }
} catch (Throwable $e) { fwrite(STDERR, 'ERROR: '.$e->getMessage()."\n"); exit(1); }
