#!/usr/bin/env python3
"""Solid task CLI. Python 3.9+, standard library only. See guide.md."""
import argparse
import json
import os
from pathlib import Path
import sys
import time
import urllib.error
import urllib.request
import uuid

BASE = 'https://solid.tech/api/workspace'

class NoRedirect(urllib.request.HTTPRedirectHandler):
    def redirect_request(self, req, fp, code, msg, headers, newurl):
        return None

def api(method, path, body=None, key=None):
    base = os.environ.get('SOLID_WORKSPACE_API_URL', BASE).rstrip('/')
    if base != BASE:
        raise ValueError('Unexpected API URL; use the documented Solid workspace origin.')
    token = os.environ.get('SOLID_WORKSPACE_API_TOKEN')
    if not token:
        raise ValueError('Load solid.env first; SOLID_WORKSPACE_API_TOKEN is missing.')
    headers = {'Authorization': 'Bearer ' + token}
    if key:
        headers['Idempotency-Key'] = key
    data = None
    if body is not None:
        headers['Content-Type'] = 'application/json'
        data = json.dumps(body, ensure_ascii=False, separators=(',', ':')).encode()
    req = urllib.request.Request(base + path, data=data, headers=headers, method=method)
    try:
        with urllib.request.build_opener(NoRedirect).open(req, timeout=60) as response:
            return json.load(response)
    except urllib.error.HTTPError as exc:
        # Do not print request headers or token, even when reporting an API error.
        try:
            problem = json.loads(exc.read())
            code = problem.get('code', 'unknown')
        except (ValueError, UnicodeDecodeError):
            code = 'unknown'
        raise RuntimeError(f'HTTP {exc.code}: {code}. Request state retained.') from None

def save(path, value, exclusive=False):
    flags = os.O_WRONLY | os.O_CREAT | (os.O_EXCL if exclusive else os.O_TRUNC)
    with os.fdopen(os.open(path, flags, 0o600), 'w') as f:
        json.dump(value, f, indent=2)
        f.flush()
        os.fsync(f.fileno())

def send(state, path):
    # The immutable request is saved before sending. An uncertain POST is retried
    # with the exact original key/body/path. Never create a replacement request.
    if 'accepted' not in state:
        accepted = api('POST', '/agents', state['request'], state['key'])
        # Separate receipt preserves the original request even if interrupted.
        save(str(path) + '.accepted.json', accepted)
        state['accepted'] = accepted
    return state['accepted']

def load(path):
    state = json.loads(Path(path).read_text())
    receipt = Path(str(path) + '.accepted.json')
    if receipt.exists():
        state['accepted'] = json.loads(receipt.read_text())
    return state

def agent_id(state):
    if 'accepted' not in state:
        raise ValueError('No acceptance receipt. Use retry with this same state file.')
    return state['accepted']['id']

def result(aid):
    runs = []
    path = f'/agents/{aid}/runs'
    while True:
        page = api('GET', path)
        for header in page['items']:
            runs.append(api('GET', f"/agents/{aid}/runs/{header['sequence']}"))
        cursor = page['next_before_sequence']
        if cursor is None:
            break
        path = f'/agents/{aid}/runs?before_sequence={cursor}'
    return sorted(runs, key=lambda r: r['sequence'])

def main():
    p = argparse.ArgumentParser(description=__doc__)
    sub = p.add_subparsers(dest='command', required=True)
    start = sub.add_parser('start', help='Create a fresh Solid agent from a prompt file')
    start.add_argument('prompt_file')
    start.add_argument('--state', required=True, help='New path; save it for retries and polling')
    for name in ('retry', 'status', 'wait', 'result', 'stop'):
        cmd = sub.add_parser(name)
        cmd.add_argument('state')
        if name == 'wait':
            cmd.add_argument('--timeout', type=float, default=1800)
            cmd.add_argument('--interval', type=float, default=10)
    args = p.parse_args()
    if args.command == 'start':
        sender = os.environ.get('SOLID_SENDER_AGENT_ID')
        if not sender:
            raise ValueError('SOLID_SENDER_AGENT_ID is required (an existing workspace agent).')
        prompt = Path(args.prompt_file).read_text().strip()
        if not prompt or len(prompt) > 200000:
            raise ValueError('Prompt must be 1–200,000 characters after trimming.')
        state = {'key': str(uuid.uuid4()), 'request': {'sender_agent_id': sender, 'text': prompt}}
        save(args.state, state, exclusive=True)
        print(json.dumps(send(state, args.state), indent=2))
        return 0
    state = load(args.state)
    if args.command == 'retry':
        print(json.dumps(send(state, args.state), indent=2))
        return 0
    aid = agent_id(state)
    if args.command == 'status':
        output = api('GET', f'/agents/{aid}')
    elif args.command == 'result':
        output = result(aid)
    elif args.command == 'stop':
        stop_path = str(args.state) + '.stop.json'
        if not Path(stop_path).exists():
            save(stop_path, {'key': str(uuid.uuid4())}, exclusive=True)
        stop = json.loads(Path(stop_path).read_text())
        output = api('POST', f'/agents/{aid}/stop', key=stop['key'])
    else:
        if args.timeout <= 0 or args.interval <= 0:
            raise ValueError('Timeout and interval must be positive.')
        deadline = time.monotonic() + args.timeout
        while True:
            summary = api('GET', f'/agents/{aid}')
            status = summary['status']
            print(f'{aid}: {status}', file=sys.stderr)
            if status in ('completed', 'interrupted', 'failed') and not summary.get('retry_at'):
                print(json.dumps({'agent': summary, 'runs': result(aid)}, indent=2))
                return 0 if status == 'completed' else 1
            if time.monotonic() >= deadline:
                print('Wait timed out; Solid continues working. Resume with wait.', file=sys.stderr)
                return 2
            time.sleep(min(args.interval, max(0, deadline - time.monotonic())))
    print(json.dumps(output, indent=2))
    return 0

if __name__ == '__main__':
    try:
        sys.exit(main())
    except (ValueError, OSError, RuntimeError) as exc:
        print(str(exc), file=sys.stderr)
        sys.exit(1)
