"""Seer research quickstart. Python 3.10+, standard library only.

Set SEER_API_BASE_URL and SEER_ACCESS_TOKEN from connection settings.
Run: python3 seer_client.py
Resume a saved invocation: python3 seer_client.py --resume seer-request-<uuid>.json
This client invokes market.bars only. It does not submit financial actions.
"""
from __future__ import annotations
import argparse
from datetime import datetime, timezone
from email.utils import parsedate_to_datetime
import json
import math
import os
from pathlib import Path
import random
import socket
import time
import uuid
from urllib.error import HTTPError, URLError
from urllib.parse import urlencode, urlsplit
from urllib.request import Request, build_opener, HTTPRedirectHandler

class SeerError(RuntimeError):
    pass

class NoRedirect(HTTPRedirectHandler):
    def redirect_request(self, req, fp, code, msg, headers, newurl):
        return None  # Never forward an access token to a redirect target.

def retry_seconds(value: str | None, attempt: int) -> float:
    if value:
        try:
            seconds = float(value)
        except ValueError:
            try:
                dt = parsedate_to_datetime(value)
                if dt.tzinfo is None:
                    dt = dt.replace(tzinfo=timezone.utc)
                seconds = dt.timestamp() - time.time()
            except (ValueError, TypeError, OverflowError):
                seconds = random.uniform(0, min(8, 2 ** attempt))
        return max(0, seconds) if math.isfinite(seconds) else random.uniform(0, min(8, 2 ** attempt))
    return random.uniform(0, min(8, 2 ** attempt))

class Client:
    def __init__(self, base: str, token: str):
        u = urlsplit(base)
        if u.scheme != 'https' or not u.netloc or u.username or u.password or u.query or u.fragment:
            raise SeerError('Use the HTTPS base URL issued in connection settings.')
        self.base = base.rstrip('/')
        self.token = token
        self.opener = build_opener(NoRedirect())

    def request(self, method, path, payload=None, key=None, deadline=None, key_expires_at=None):
        u = urlsplit(path)
        if u.scheme or u.netloc or not path.startswith('/v1/') or '..' in u.path.split('/'):
            raise SeerError('Refusing a path outside the issued API resource namespace.')
        body = None if payload is None else json.dumps(payload, separators=(',', ':')).encode()
        headers = {'Authorization': 'Bearer ' + self.token, 'Accept': 'application/json'}
        if body is not None:
            headers['Content-Type'] = 'application/json'
        if key:
            headers['Idempotency-Key'] = key
        if method == 'POST' and (not key or key_expires_at is None):
            raise SeerError('Creation needs a persisted key and conservative expiry.')
        deadline = deadline if deadline is not None else time.monotonic() + 60
        for attempt in range(4):
            remaining = deadline - time.monotonic()
            if remaining <= 0:
                raise SeerError('Client deadline reached; admitted work may still be running.')
            if method == 'POST' and time.time() >= key_expires_at:
                raise SeerError('Saved retry window expired. Recover the existing invocation before starting new work.')
            try:
                req = Request(self.base + path, data=body, headers=headers, method=method)
                with self.opener.open(req, timeout=min(20, remaining)) as response:
                    data = json.load(response)
                    return response.status, dict(response.headers.items()), data
            except HTTPError as exc:
                try:
                    problem = json.loads(exc.read())
                except (ValueError, UnicodeDecodeError):
                    problem = {}
                if exc.code not in (429, 503) or problem.get('retry') != 'after_delay':
                    raise SeerError(f'HTTP {exc.code}: {problem.get("code", "REQUEST_FAILED")} '
                                    f'(request {problem.get("request_id", "unavailable")})') from None
                delay = retry_seconds(exc.headers.get('Retry-After'), attempt)
            except (URLError, TimeoutError, socket.timeout):
                # POST retries preserve exactly the same payload and key.
                delay = retry_seconds(None, attempt)
            if attempt == 3 or time.monotonic() + delay >= deadline:
                raise SeerError('Retry budget exhausted. Keep the saved request identity for recovery.')
            time.sleep(delay)
        raise SeerError('Unreachable retry state')

    def collection(self, path, filters=None):
        filters = dict(filters or {})
        cursor = None
        seen = set()
        for _ in range(100):
            query = {**filters, 'limit': 100}
            if cursor is not None:
                query['cursor'] = cursor
            _, _, data = self.request('GET', path + '?' + urlencode(query))
            yield from data['items']
            cursor = data['next_cursor']
            if cursor is None:
                return
            if cursor in seen:
                raise SeerError('Repeated pagination cursor; refusing an infinite traversal.')
            seen.add(cursor)
        raise SeerError('Pagination safety bound reached. Narrow the discovery query.')

    def finish(self, status, data, deadline=None):
        if status == 200:
            return data
        if status != 202 or not data.get('job_id'):
            raise SeerError('Unexpected invocation response.')
        job_id = data['job_id']
        print('Admitted job:', job_id)
        path = data['status_url']
        if path != '/v1/jobs/' + job_id:
            raise SeerError('Unexpected job path; recover using the recorded job ID.')
        deadline = deadline if deadline is not None else time.monotonic() + 120
        while time.monotonic() < deadline:
            _, headers, job = self.request('GET', path, deadline=deadline)
            state = job['state']
            if state == 'completed':
                _, _, artifact = self.request('GET', '/v1/artifacts/' + job['result_ref'], deadline=deadline)
                if artifact['kind'] != 'tool_result' or artifact['schema_revision'] != '1.0':
                    raise SeerError('Unsupported result artifact contract.')
                return artifact['data']
            if state in ('failed', 'cancelled'):
                raise SeerError(f'Job {job_id}: {state}; {job.get("error_code", "no result")}')
            if state not in ('accepted', 'running', 'waiting'):
                raise SeerError('Unsupported job state; inspect the resource before proceeding.')
            delay = max(1, retry_seconds(headers.get('Retry-After'), 0))
            if time.monotonic() + delay >= deadline:
                break
            time.sleep(delay)
        raise SeerError(f'Polling deadline reached. Job {job_id} remains recoverable; it was not cancelled.')

def main():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument('--resume', type=Path)
    args = parser.parse_args()
    client = Client(os.environ['SEER_API_BASE_URL'], os.environ['SEER_ACCESS_TOKEN'])
    _, _, connection = client.request('GET', '/v1/connection')
    needed = {'connection:read','agents:read','instruments:read','tools:read','tools:invoke','market:read','jobs:read','artifacts:read'}
    missing = needed - set(connection['scopes'])
    if missing:
        raise SeerError('Missing connection scopes: ' + ', '.join(sorted(missing)))
    if args.resume:
        saved = json.loads(args.resume.read_text())
        if saved['connection_id'] != connection['connection_id'] or saved['base_url'] != client.base:
            raise SeerError('Saved invocation belongs to a different connection or service.')
        if saved['path'] != '/v1/tool-invocations' or saved['payload']['tool'] != 'market.bars':
            raise SeerError('This walkthrough resumes market.bars invocations only.')
    else:
        revision_id = connection['default_agent_revision_id']
        if revision_id is None:
            raise SeerError('Bind a research agent revision in connection settings first.')
        _, _, revision = client.request('GET', '/v1/agent-revisions/' + revision_id)
        manifests = [m for m in client.collection('/v1/tools') if m['name'] == 'market.bars']
        if len(manifests) != 1 or revision['tool_versions'].get('market.bars') != manifests[0]['schema_revision']:
            raise SeerError('Enable market.bars at the served schema revision on this agent.')
        instruments = list(client.collection('/v1/instruments', {'query':'AAPL','venue':'XNAS','kind':'equity'}))
        matches = [i for i in instruments if i['symbol']=='AAPL' and i['venue']=='XNAS' and i['kind']=='equity']
        if len(matches) != 1:
            raise SeerError('No unique AAPL equity listing on XNAS; inspect instrument discovery.')
        key = str(uuid.uuid4())
        saved = {'base_url':client.base,'connection_id':connection['connection_id'],
                 'path':'/v1/tool-invocations','key':key,
                 'key_expires_at':time.time()+connection['limits']['idempotency_retention_seconds'],
                 'payload':{'agent_revision_id':revision_id,'tool':'market.bars','input':{'instrumentId':matches[0]['instrument_id']}}}
        args.resume = Path('seer-request-' + key + '.json')
        with args.resume.open('x') as f:
            json.dump(saved, f, indent=2)
    print('Request identity saved in:', args.resume)
    status, headers, data = client.request('POST', saved['path'], saved['payload'], saved['key'], key_expires_at=saved['key_expires_at'])
    result = client.finish(status, data)
    if result.get('tool') != 'market.bars' or result.get('schema_version') != '1.0':
        raise SeerError('Unexpected tool result contract.')
    if result['status'] not in ('complete','partial'):
        raise SeerError('Unsupported coverage state.')
    print(json.dumps({'tool':result['tool'],'status':result['status'],
        'instrument_id':result['data']['instrumentId'],'observation_count':len(result['data']['observations']),
        'evidence_count':len(result['evidence']),'warnings':result['warnings'],'coverage':result['coverage']},indent=2))
    if result['next_cursor'] is not None:
        print('More tool data is available. Request that cursor as a new invocation, keeping the same filters.')

if __name__ == '__main__':
    try:
        main()
    except (SeerError, KeyError, ValueError) as exc:
        raise SystemExit(str(exc)) from None
