#!/usr/bin/env python3
"""Outbound TeleIn text bridge for an installed, configured OpenClaw gateway.

Run one connector per entity token. Sessions are scoped by server, token and topic.
The private journal prevents automatic replay after uncertain agent failures.
"""
import argparse
import getpass
import hashlib
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, HTTPRedirectHandler, build_opener


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


class NoRedirect(HTTPRedirectHandler):
    def redirect_request(self, *args):
        raise ValueError('Redirect refused. Use the final HTTPS server address.')


def save(path, value):
    fd, temporary = tempfile.mkstemp(dir=path.parent)
    try:
        os.fchmod(fd, 0o600)
        with os.fdopen(fd, 'w') as stream:
            json.dump(value, stream)
            stream.flush()
            os.fsync(stream.fileno())
        os.replace(temporary, path)
    finally:
        if os.path.exists(temporary):
            os.unlink(temporary)


def reply_text(output):
    value = json.loads(output)
    if value.get('ok') is False or value.get('status') in ('error', 'timeout', 'cancelled'):
        raise ValueError('OpenClaw reported an unsuccessful turn.')
    result = value.get('result', value)
    payloads = result.get('payloads', [])
    text = '\n\n'.join(p['text'] for p in payloads if isinstance(p.get('text'), str) and p['text'])
    if not text or len(text) > 32000:
        raise ValueError('OpenClaw did not return a supported text reply.')
    return text


def process(update, agent):
    message = update.get('message') or update.get('callback_query', {}).get('message', {})
    thread = message.get('message_thread_id')
    if not thread:
        raise ValueError('Update has no topic; refusing to route it to another session.')
    if 'callback_query' in update or 'photo' in message or 'document' in message:
        return thread, 'This OpenClaw connector supports text only. Please send a text message.'
    text = message.get('text', '')
    if not text or text.startswith('/'):
        return thread, 'OpenClaw slash commands and cancellation are not supported by this connector. Use its local controls.'
    return thread, agent(thread, text)


def main():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument('--server', default='https://telein.ewizt.com')
    parser.add_argument('--agent', default='main')
    parser.add_argument('--state-dir', type=Path, default=Path.home() / '.telein-openclaw')
    args = parser.parse_args()
    server = origin(args.server)
    binary = shutil.which('openclaw')
    if not binary:
        raise ValueError('Install and configure OpenClaw first: https://docs.openclaw.ai/start/getting-started')
    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 entity connection token from Settings.')
    opener = build_opener(NoRedirect)
    def api(method, **body):
        request = Request(server + '/bot' + token + '/' + method, data=json.dumps(body).encode(), headers={'Content-Type': 'application/json'})
        try:
            with opener.open(request, timeout=45) as response:
                result = json.load(response)
            if not result.get('ok'):
                raise ValueError()
            return result['result']
        except Exception:
            raise ValueError('TeleIn request failed. Check connectivity, TLS and the entity token; no automatic replay was attempted.') from None
    if not api('getMe').get('has_topics_enabled'):
        raise ValueError('This server is not compatible with TeleIn.')
    identity = hashlib.sha256((server + '\n' + token).encode()).hexdigest()[:20]
    args.state_dir.mkdir(parents=True, exist_ok=True, mode=0o700)
    journal = args.state_dir / (identity + '.json')
    lock = args.state_dir / (identity + '.lock')
    try:
        descriptor = os.open(lock, os.O_CREAT | os.O_EXCL | os.O_WRONLY, 0o600)
    except FileExistsError:
        raise ValueError('Connector lock exists. Stop the previous connector before removing a stale lock.') from None
    os.close(descriptor)
    try:
        state = json.loads(journal.read_text()) if journal.exists() else {'offset': 0}
        if state.get('pending'):
            raise ValueError('An earlier turn has uncertain delivery. Inspect the OpenClaw session and TeleIn topic before manually reconciling the private journal. Refusing automatic replay.')
        api('setTeleInProfile', profile='openclaw-' + identity[:12], provider='openclaw')
        def agent(thread, text):
            session = 'telein-' + identity + '-' + str(thread)
            output = subprocess.run([binary, 'agent', '--agent', args.agent, '--session-id', session, '--message', text, '--json', '--timeout', '120'], text=True, capture_output=True, timeout=150, check=True)
            return reply_text(output.stdout)
        print('Connected for text conversations. Keep this terminal open; check the connection in TeleIn.')
        while True:
            updates = api('getUpdates', offset=state['offset'], timeout=30)
            for update in updates:
                state['pending'] = update['update_id']
                save(journal, state)
                thread, text = process(update, agent)
                api('sendMessage', chat_id=1001, message_thread_id=thread, text=text)
                state = {'offset': update['update_id'] + 1}
                save(journal, state)
    finally:
        lock.unlink(missing_ok=True)


if __name__ == '__main__':
    try:
        main()
    except KeyboardInterrupt:
        sys.exit(130)
    except Exception as error:
        # Never print subprocess arguments, token-bearing URLs or model output.
        print(str(error) if isinstance(error, ValueError) else 'Connector stopped. Inspect local state before retrying; sensitive error details were suppressed.', file=sys.stderr)
        sys.exit(1)
