"""Sequential inventory research using bounded SoldFetch background jobs.

No requests are made on import. Each keyword is one resumable job. Keep the output
folder: manifests hold job IDs and stable start idempotency keys, never API keys.
"""
import argparse
import json
import os
from pathlib import Path
import time
from urllib.error import HTTPError
from urllib.request import Request, build_opener, HTTPRedirectHandler
import uuid


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


class Api:
    def __init__(self, key):
        self.key, self.opener = key, build_opener(NoRedirect())

    def __call__(self, method, path, body=None, idempotency_key=None, csv=False):
        if not path.startswith('/v1/'):
            raise ValueError('Unexpected API path')
        headers = {'API-KEY': self.key, 'Accept': 'text/csv' if csv else 'application/json'}
        data = None
        if body is not None:
            data = json.dumps(body).encode()
            headers['Content-Type'] = 'application/json'
        if idempotency_key:
            headers['Idempotency-Key'] = idempotency_key
        request = Request('https://soldfetch.com' + path, data=data, headers=headers, method=method)
        try:
            with self.opener.open(request, timeout=30) as response:
                content = response.read()
                return content if csv else json.loads(content)
        except HTTPError as exc:
            delay = exc.headers.get('Retry-After', 'unspecified')
            raise RuntimeError(f'HTTP {exc.code}; Retry-After={delay}. Keep the manifest and inspect before resuming.') from None


def save_manifest(path, manifest):
    temporary = path.with_suffix('.tmp')
    temporary.write_text(json.dumps(manifest, indent=2) + '\n')
    temporary.replace(path)


def run_job(api, body, manifest_path, csv_path, wait_seconds=600, poll_seconds=10,
            clock=time.monotonic, sleep=time.sleep):
    if not 1 <= body.get('maxPages', 0) <= 100 or not 1 <= body.get('count', 0) <= 200:
        raise ValueError('maxPages must be 1–100 and count 1–200')
    if poll_seconds < 10 or wait_seconds < poll_seconds:
        raise ValueError('Allow at least 10 seconds between polls and one poll interval of waiting')
    if manifest_path.exists():
        manifest = json.loads(manifest_path.read_text())
        if manifest['input'] != body:
            raise ValueError('Inputs differ from saved manifest; choose a new output folder')
    else:
        manifest = {'input': body, 'idempotencyKey': str(uuid.uuid4()), 'jobId': None,
                    'qualityNote': 'Displayed prices; accepted offers, sale dates, and exact product matches are unverified.'}
        save_manifest(manifest_path, manifest)
    if not manifest['jobId']:
        started = api('POST', '/v1/scrape/max', body=body, idempotency_key=manifest['idempotencyKey'])
        job_id = str(uuid.UUID(started['jobId']))
        manifest.update(jobId=job_id, startResponse=started)
        save_manifest(manifest_path, manifest)
    job_id = str(uuid.UUID(manifest['jobId']))
    results_path = '/v1/scrape/max/results/' + job_id
    stop_at = clock() + wait_seconds
    try:
        while clock() < stop_at:
            result = api('GET', results_path)
            manifest['lastResult'] = result
            save_manifest(manifest_path, manifest)
            state = result['status']
            if state == 'complete':
                content = api('GET', '/v1/scrape/max/' + job_id + '/download.csv', csv=True)
                csv_path.write_bytes(content)
                manifest['csvPath'] = str(csv_path)
                save_manifest(manifest_path, manifest)
                return result
            if state in {'failed', 'cancelled'}:
                raise RuntimeError(f'Job {state}; inspect lastResult and error in {manifest_path}')
            if state not in {'queued', 'running'}:
                raise RuntimeError('Unknown job status; keep manifest and inspect')
            sleep(min(poll_seconds, max(0, stop_at - clock())))
        raise TimeoutError('Local polling deadline reached')
    except (TimeoutError, KeyboardInterrupt):
        # Cancellation can race an in-flight page. Existing successful work is billable.
        try:
            manifest['lastResult'] = api('DELETE', '/v1/scrape/max/' + job_id)
        except Exception as exc:
            manifest['cancelError'] = str(exc)
        save_manifest(manifest_path, manifest)
        raise


def main():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument('keywords_file', type=Path, help='One explicit query per line; at most 20')
    parser.add_argument('--output', type=Path, required=True)
    parser.add_argument('--site', choices=['ebay.com','ebay.co.uk','ebay.de'], default='ebay.com')
    parser.add_argument('--max-pages', type=int, default=3)
    parser.add_argument('--count', type=int, default=60)
    parser.add_argument('--wait-seconds', type=int, default=600)
    args = parser.parse_args()
    keywords = list(dict.fromkeys(line.strip() for line in args.keywords_file.read_text().splitlines() if line.strip()))
    if not 1 <= len(keywords) <= 20 or any(len(k) > 200 for k in keywords):
        parser.error('Supply 1–20 distinct queries with at most 200 characters each')
    if not 1 <= args.max_pages <= 100 or not 1 <= args.count <= 200 or not 10 <= args.wait_seconds <= 3600:
        parser.error('max-pages=1–100, count=1–200, wait-seconds=10–3600')
    key = os.environ.get('SOLDFETCH_API_KEY', '').strip()
    if not key:
        parser.error('Set SOLDFETCH_API_KEY in the environment')
    api = Api(key)
    caps = api('GET', '/v1/capabilities')
    if not caps.get('dataEnabled') or args.site not in caps.get('enabledSites', []):
        parser.error('Data or the selected site is currently unavailable')
    args.output.mkdir(parents=True, exist_ok=True)
    print(f'Maximum successful page units for this input: {len(keywords) * args.max_pages}')
    for index, keyword in enumerate(keywords, 1):
        body = {'keyword': keyword, 'ebaySite': args.site, 'sold': True,
                'itemCondition': 'used', 'count': args.count, 'maxPages': args.max_pages,
                'resultType': 'download'}
        prefix = args.output / f'query-{index:02d}'
        result = run_job(api, body, prefix.with_suffix('.json'), prefix.with_suffix('.csv'), args.wait_seconds)
        print(f'{keyword}: {result["totalItems"]} rows, {result["units"]} units, expires {result["expiresAt"]}')


if __name__ == '__main__':
    try:
        main()
    except (RuntimeError, TimeoutError) as exc:
        raise SystemExit(str(exc)) from None
