"""Generate docs/openapi.json from docs/api-inventory.json.

The inventory is a list of {surface, endpoints:[{method,path,auth,params,purpose,
example}]} objects (one per API surface). Re-run after adding/changing endpoints
(update api-inventory.json first, or regenerate it). Served interactively at
/api/docs (see shopdb/core/api/docs.py).

    venv/bin/python scripts/gen_openapi.py
"""

import json
import os
import re

HERE = os.path.dirname(os.path.abspath(__file__))
REPO = os.path.dirname(HERE)
INVENTORY = os.path.join(REPO, 'docs', 'api-inventory.json')
OUT = os.path.join(REPO, 'docs', 'openapi.json')

VERBS = ('get', 'post', 'put', 'patch', 'delete', 'head', 'options')


def _product_version():
    """Read __version__ out of shopdb/__init__.py without importing the app.

    Importing shopdb here would pull in Flask, the plugin loader and a database
    configuration, none of which this generator needs.
    """
    here = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
    init = os.path.join(here, 'shopdb', '__init__.py')
    with open(init, 'r', encoding='utf-8') as handle:
        match = re.search(r"^__version__ = '([^']+)'", handle.read(), re.M)
    if not match:
        raise SystemExit('could not read __version__ from ' + init)
    return match.group(1)


def _norm_path(p):
    # Flask <int:id> / <id> -> OpenAPI {id}
    return re.sub(r'<(?:[^:>]+:)?([^>]+)>', r'{\1}', p or '')


def _security(auth):
    a = (auth or '').lower()
    if a in ('none', 'public', ''):
        return []
    if 'api-key' in a or 'api_key' in a or 'x-api-key' in a:
        return [{'apiKeyAuth': []}]
    return [{'bearerAuth': []}]


def build(surfaces):
    paths = {}
    tags = []
    for s in surfaces:
        tags.append({'name': s['surface']})
        for e in s.get('endpoints', []):
            for verb in re.split(r'[\/,]', (e.get('method') or 'GET')):
                verb = verb.strip().lower()
                if verb not in VERBS:
                    continue
                path = _norm_path(e.get('path'))
                if not path:
                    continue
                desc = []
                if e.get('purpose'):
                    desc.append(e['purpose'])
                if e.get('auth'):
                    desc.append('\n\n**Auth:** ' + e['auth'])
                if e.get('params'):
                    desc.append('\n\n**Params:** ' + e['params'])
                if e.get('example'):
                    desc.append('\n\n**Example:**\n```\n' + e['example'] + '\n```')
                paths.setdefault(path, {})[verb] = {
                    'tags': [s['surface']],
                    'summary': (e.get('purpose') or path)[:120],
                    'description': ''.join(desc),
                    'security': _security(e.get('auth')),
                    'responses': {'200': {
                        'description': 'Success (success_response envelope)'}},
                }
    return {
        'openapi': '3.1.0',
        'info': {
            'title': 'ShopDB Flask API',
            # Read from the code, not restated. A hardcoded copy here had
            # already drifted a release behind, and the same mistake in the
            # installer script shipped an exe stamped with the wrong version.
            'version': _product_version(),
            'description': (
                'Asset-management API (core + plugins). Responses use a '
                '`success_response` envelope: `{status, data, meta}`. Auth: Bearer '
                'JWT (login or a managed PAT) for `jwt`/`admin`/`permission:*`; '
                '`X-API-Key` for collector/managed-token endpoints; public endpoints '
                'need neither.'),
        },
        'servers': [
            {'url': 'https://tsgwp00525.wjs.geaerospace.net/shopdb', 'description': 'WJ prod'},
            {'url': '/', 'description': 'relative to the deployed mount'},
        ],
        'components': {'securitySchemes': {
            'bearerAuth': {'type': 'http', 'scheme': 'bearer', 'bearerFormat': 'JWT',
                           'description': 'Login token or a managed Personal Access Token (scoped).'},
            'apiKeyAuth': {'type': 'apiKey', 'in': 'header', 'name': 'X-API-Key',
                           'description': 'Managed service token (e.g. collector.ingest, geenforce.fetch).'},
        }},
        'tags': tags,
        'paths': paths,
    }


def main():
    surfaces = json.load(open(INVENTORY))
    spec = build(surfaces)
    json.dump(spec, open(OUT, 'w'), indent=1)
    ops = sum(len(v) for v in spec['paths'].values())
    print('wrote %s: %d paths, %d operations from %d surfaces'
          % (OUT, len(spec['paths']), ops, len(surfaces)))


if __name__ == '__main__':
    main()
