#!/usr/bin/env python3
"""Install and connect a dedicated Hermes profile. Python 3.10+, Linux/macOS/WSL."""
import argparse
import getpass
import json
import os
from pathlib import Path
import re
import shutil
import subprocess
import sys
import tempfile
from urllib.parse import urlparse
from urllib.request import Request, urlopen

DEFAULT_SERVER = 'https://telein.ewizt.com'
REPO_ROOT = Path(__file__).resolve().parent
INSTALLER = 'https://hermes-agent.nousresearch.com/install.sh'

def origin(value):
    if '://' not in value:
        value = 'https://' + value
    parsed = urlparse(value)
    if parsed.scheme != 'https' or not parsed.hostname or parsed.username or parsed.password or parsed.query or parsed.fragment or parsed.path not in ('', '/'):
        raise ValueError('Use an HTTPS hostname or IP without credentials, path, query or fragment.')
    return value.rstrip('/')

def run(binary, args, capture=False):
    return subprocess.run([binary, *args], check=True, text=True, stdout=subprocess.PIPE if capture else None).stdout

def save_token(env_path, token):
    env_path = Path(env_path)
    if not env_path.is_absolute():
        raise ValueError('Hermes returned an invalid secrets path')
    env_path.parent.mkdir(parents=True, exist_ok=True)
    if env_path.is_symlink():
        raise ValueError('Refusing to replace a symlink secrets file')
    text = env_path.read_text() if env_path.exists() else ''
    lines = [line for line in text.splitlines() if not re.match(r'^\s*(?:export\s+)?TELEGRAM_BOT_TOKEN\s*=', line)]
    lines.append('TELEGRAM_BOT_TOKEN=' + json.dumps(token))
    fd, temporary = tempfile.mkstemp(dir=env_path.parent)
    try:
        os.fchmod(fd, 0o600)
        with os.fdopen(fd, 'w') as file:
            file.write('\n'.join(lines) + '\n')
        os.replace(temporary, env_path)
    finally:
        if os.path.exists(temporary):
            os.unlink(temporary)

def main():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument('action', nargs='?', default='connect', choices=['connect', 'status', 'start', 'stop', 'restart', 'model', 'cleanup'])
    parser.add_argument('--server', default=DEFAULT_SERVER)
    parser.add_argument('--profile', help='Dedicated profile; inferred from the token for connect')
    parser.add_argument('--foreground', action='store_true', help='Run instead of installing a managed service')
    args = parser.parse_args()
    if args.profile and (not re.fullmatch(r'[a-z][a-z0-9-]{0,31}', args.profile) or args.profile == 'default'):
        parser.error('Use a dedicated profile, not default')
    binary = shutil.which('hermes')
    if args.action != 'connect':
        if not binary or not args.profile:
            parser.error('Hermes and --profile are required for management commands')
        if args.action == 'cleanup':
            import hermes_constants
            envs = {'HERMES_PROFILE': args.profile}
            run(binary, ['-p', args.profile, 'cron', 'run', 'telein-session-cleanup'])
            return
        run(binary, ['-p', args.profile, 'model'] if args.action == 'model' else ['-p', args.profile, 'gateway', args.action])
        return
    server = origin(args.server)
    token = getpass.getpass('Paste the entity token from TeleIn: ').strip()
    if not re.fullmatch(r'\d+:[a-f0-9]{64}', token):
        raise ValueError('Use the token generated in entity connection settings')
    # No credential-bearing command line, logging, or redirect to another host.
    from urllib.request import HTTPRedirectHandler, build_opener
    class NoRedirect(HTTPRedirectHandler):
        def redirect_request(self, *unused):
            raise ValueError('Server redirects are not accepted; enter its final HTTPS address')
    opener = build_opener(NoRedirect)
    try:
        with opener.open(Request(server + '/bot' + token + '/getMe'), timeout=20) as response:
            identity = json.load(response)
    except Exception:
        raise ValueError('Cannot authenticate to TeleIn. Check server, token, TLS and outbound access.') from None
    if not identity.get('ok') or not identity.get('result', {}).get('has_topics_enabled'):
        raise ValueError('Server is not a compatible TeleIn endpoint')
    import hashlib
    profile = args.profile or 'telein-' + hashlib.sha256(token.encode()).hexdigest()[:12]
    if not binary:
        if os.name == 'nt':
            raise ValueError('Run this wrapper inside WSL on Windows')
        print('Installing Hermes using its official installer...')
        with urlopen(INSTALLER, timeout=30) as response:
            installer = response.read()
        with tempfile.TemporaryDirectory() as directory:
            script = Path(directory) / 'install.sh'
            script.write_bytes(installer)
            run('bash', [str(script), '--non-interactive'])
        binary = shutil.which('hermes')
        for candidate in [Path.home() / '.local/bin/hermes', Path.home() / '.hermes/hermes-agent/venv/bin/hermes']:
            if not binary and candidate.is_file():
                binary = str(candidate)
        if not binary:
            raise ValueError('Installer finished but hermes was not found. Add it to PATH and rerun.')
    listing = run(binary, ['profile', 'list'], capture=True)
    created = not re.search(r'(?<![a-z0-9-])' + re.escape(profile) + r'(?![a-z0-9-])', listing)
    if created:
        profile_help = run(binary, ['profile', 'create', '--help'], capture=True)
        if '--clone-channels' not in profile_help:
            raise ValueError('Update Hermes before connecting TeleIn; this version cannot safely reuse model credentials without copying messaging channels.')
        run(binary, ['profile', 'create', profile, '--clone-from', 'default', '--no-alias'])
    prefix = ['-p', profile]
    # CLI updates preserve unrelated provider, memory and tool configuration.
    for key, value in {
        'gateway.standalone': 'true', 'platforms.telegram.enabled': 'true',
        'platforms.telegram.reply_to_mode': 'off',
        'platforms.telegram.extra.base_url': server + '/bot',
        'platforms.telegram.extra.base_file_url': server + '/file/bot',
        'platforms.telegram.extra.drop_pending_on_cold_boot': 'false',
        'streaming.enabled': 'false',
        'TELEGRAM_ALLOWED_USERS': '1001', 'TELEGRAM_HOME_CHANNEL': '1001'
    }.items():
        run(binary, prefix + ['config', 'set', key, value])
    env_path = run(binary, prefix + ['config', 'env-path'], capture=True).strip()
    save_token(env_path, token)
    run(binary, prefix + ['chat', '--oneshot', '-q', 'Reply with MODEL_OK and nothing else.'])
    # Install the topic-deletion cleanup: the profile cron runs the bundled
    # script every 5 minutes; it deletes sessions for topics the server deleted.
    scripts_dir = Path.home() / '.hermes' / 'profiles' / profile / 'scripts'
    repo_scripts = Path(__file__).resolve().parent / 'scripts'
    if repo_scripts.is_dir():
        scripts_dir.mkdir(parents=True, exist_ok=True)
        shutil.copy(repo_scripts / 'telein_cleanup.py', scripts_dir / 'telein_cleanup.py')
        # Teach the profile's agents the TeleIn secure-prompt flow (future sessions included).
        try:
            from importlib.util import spec_from_file_location, module_from_spec
            spec = spec_from_file_location('telein_skill', repo_scripts / 'telein_skill.py')
            skill_mod = module_from_spec(spec); spec.loader.exec_module(skill_mod)
            skill_dir = Path.home() / '.hermes' / 'profiles' / profile / 'skills' / 'telein-secrets'
            skill_dir.mkdir(parents=True, exist_ok=True)
            (skill_dir / 'SKILL.md').write_text(skill_mod.SKILL_MD)
        except Exception as skill_error:
            print(f'Skill install skipped: {skill_error}')
        # Maintenance: a systemd user timer runs telein_maint.sh every 2 minutes
        # OUTSIDE the gateway — token rekey (server says the token was replaced:
        # swap .env, cycle the profile gateway) and deleted-topic session cleanup.
        bin_dir = Path.home() / '.hermes' / 'profiles' / profile / 'bin'
        bin_dir.mkdir(parents=True, exist_ok=True)
        maint_sh = bin_dir / 'telein_maint.sh'
        maint_src = repo_scripts / 'telein_maint.sh'
        if maint_src.is_file():
            maint_sh.write_text(maint_src.read_text())
            maint_sh.chmod(0o755)
            unit_dir = Path.home() / '.config' / 'systemd' / 'user'
            unit_dir.mkdir(parents=True, exist_ok=True)
            (unit_dir / f'telein-maint-{profile}.service').write_text(
                '[Unit]\nDescription=TeleIn maintenance\n\n[Service]\nType=oneshot\n'
                f'ExecStart={maint_sh} {profile}\n')
            (unit_dir / f'telein-maint-{profile}.timer').write_text(
                '[Unit]\nDescription=TeleIn maintenance poll\n\n[Timer]\nOnCalendar=*-*-* *:*:00\nOnUnitActiveSec=2m\nAccuracySec=30s\n'
                f'Unit=telein-maint-{profile}.service\n\n[Install]\nWantedBy=timers.target\n')
            if shutil.which('systemctl'):
                run('systemctl', ['--user', 'daemon-reload'])
                run('systemctl', ['--user', 'enable', '--now', f'telein-maint-{profile}.timer'])
    registration = Request(
        server + '/bot' + token + '/setTeleInProfile',
        data=json.dumps({'profile': profile}).encode(),
        headers={'Content-Type': 'application/json'},
        method='POST',
    )
    try:
        with opener.open(registration, timeout=20) as response:
            registered = json.load(response)
    except Exception:
        raise ValueError('Hermes is configured, but TeleIn could not save its profile name. Check the server and rerun.') from None
    if not registered.get('ok'):
        raise ValueError('TeleIn rejected the Hermes profile name')
    print('Profile:', profile)
    if args.foreground:
        run(binary, prefix + ['gateway', 'run'])
    else:
        run(binary, prefix + ['gateway', 'install'])
        run(binary, prefix + ['gateway', 'restart'])
        run(binary, prefix + ['gateway', 'status'])
        print('Connected. Return to TeleIn and send a connection check.')

if __name__ == '__main__':
    try:
        main()
    except (ValueError, subprocess.CalledProcessError) as error:
        print(str(error), file=sys.stderr)
        sys.exit(1)
    except KeyboardInterrupt:
        sys.exit(130)
