"""Generates the Postman collection and environment of the Theme 5 API from the OpenAPI documents in docs/theme5-api.

    python3 docs/theme5-api/postman/generate_postman.py

Writes Theme5-API.postman_collection.json and Theme5-local.postman_environment.json next to this file. The collection is a
view of the contract, never a second source: run this again whenever an OpenAPI document changes.
"""
import json
import pathlib

HERE = pathlib.Path(__file__).resolve().parent
API = HERE.parent
FOLDERS = [  # (document, folder name)
    ('auth.openapi.json', '0. Staff sign-in (auth)'),
    ('profile.openapi.json', '1. Client profile and calorie profile'),
    ('targets.openapi.json', '2. Calorie and meal targets'),
    ('recipes.openapi.json', '3. Recipe options and selection'),
    ('activity.openapi.json', '4. Activity records and summary'),
    ('audit.openapi.json', '5. Audit log and permissions'),
    ('suggestions.openapi.json', '6. AI suggestions (coach review)'),
]
DOCS = {name: json.loads((API / name).read_text()) for name, _ in FOLDERS}
DOCS['common.openapi.json'] = json.loads((API / 'common.openapi.json').read_text())
PATH_DEFAULTS = {'clientId': '{{clientId}}', 'week': '{{week}}', 'dayIndex': '{{dayIndex}}', 'mealType': '{{mealType}}',
                 'recordId': '{{recordId}}', 'suggestionId': '{{suggestionId}}'}
QUERY_DEFAULTS = {'from': '{{fromDate}}', 'to': '{{toDate}}', 'date': '{{date}}'}
NAMED = {'email': 'coach@example.com', 'password': 'a-strong-password', 'total': 1500, 'kcal': 500, 'expectedRevision': 1,
         'reason': 'Duplicate of another record', 'source': 'manual', 'sourceEventId': 'manual-0001', 'date': '2026-10-02',
         'steps': 8000, 'stars': 1, 'recipeId': 'r1', 'hiddenRecipeIds': ['r5']}


def resolve(ref, doc_name):
    """A $ref of the given document: '#/...' (same file) or 'common.openapi.json#/...'. Returns (node, its document name)."""
    file, _, pointer = ref.partition('#')
    target = file or doc_name
    node = DOCS[target]
    for part in pointer.strip('/').split('/'):
        node = node[part]
    return node, target


def deref(node, doc_name):
    seen = 0
    while isinstance(node, dict) and '$ref' in node and seen < 20:
        node, doc_name = resolve(node['$ref'], doc_name)
        seen += 1
    return node, doc_name


def example(schema, doc_name, name=None, depth=0):
    """A plausible example value for a JSON schema (enums, bounds, steps and field names respected)."""
    schema, doc_name = deref(schema, doc_name)
    if depth > 8 or not isinstance(schema, dict):
        return None
    if name in NAMED and 'enum' not in schema:
        return NAMED[name]
    for key in ('allOf',):
        if key in schema:
            merged = {}
            for part in schema[key]:
                value = example(part, doc_name, name, depth + 1)
                if isinstance(value, dict):
                    merged.update(value)
            return merged
    for key in ('oneOf', 'anyOf'):
        if key in schema:
            return example(schema[key][0], doc_name, name, depth + 1)
    if 'enum' in schema:
        return next((v for v in schema['enum'] if v is not None), schema['enum'][0])
    kind = schema.get('type')
    if isinstance(kind, list):
        kind = next((k for k in kind if k != 'null'), 'null')
    if kind == 'object' or 'properties' in schema:
        props = schema.get('properties', {})
        return {key: example(value, doc_name, key, depth + 1) for key, value in props.items()}
    if kind == 'array':
        item = example(schema.get('items', {}), doc_name, None, depth + 1)
        return [] if item is None else [item]
    if kind in ('integer', 'number'):
        low = schema.get('minimum', schema.get('exclusiveMinimum', 0) + (1 if 'exclusiveMinimum' in schema else 0))
        step = schema.get('multipleOf')
        if step:
            low = -(-low // step) * step
        return int(low) if kind == 'integer' else low
    if kind == 'boolean':
        return False
    if kind == 'string':
        fmt = schema.get('format')
        if fmt == 'date':
            return '2026-10-02'
        if fmt == 'date-time':
            return '2026-10-02T12:00:00.000Z'
        return 'text'
    return None


def describe_schema(schema, doc_name, depth=0):
    """Markdown lines describing the fields of a request body."""
    schema, doc_name = deref(schema, doc_name)
    lines = []
    required = set(schema.get('required', []))
    for key, value in schema.get('properties', {}).items():
        value, _ = deref(value, doc_name)
        kind = value.get('type', 'object')
        kind = '/'.join(kind) if isinstance(kind, list) else kind
        bits = [k + ' ' + json.dumps(value[k]) for k in ('enum', 'minimum', 'maximum', 'multipleOf', 'minLength', 'maxLength', 'pattern', 'format') if k in value]
        note = value.get('description', '')
        lines.append(f"- `{key}` ({kind}{', required' if key in required else ''}{'; ' + ', '.join(bits) if bits else ''}){' — ' + note if note else ''}")
    if schema.get('additionalProperties') is False:
        lines.append('- No other properties are accepted (400 VALIDATION_FAILED).')
    return lines


def request_item(path, method, op, doc_name):
    params = [deref(p, doc_name)[0] for p in op.get('parameters', [])]
    path_params = [p for p in params if p.get('in') == 'path']
    query = [p for p in params if p.get('in') == 'query']
    header_params = [p for p in params if p.get('in') == 'header']
    cookie_params = [p for p in params if p.get('in') == 'cookie']
    write = any(p.get('name') == 'Idempotency-Key' for p in header_params)
    secured = bool(op.get('security'))
    segments = [(':' + s[1:-1]) if s.startswith('{') else s for s in path.strip('/').split('/')]
    headers = [{'key': 'Accept', 'value': 'application/json'}]
    body = None
    body_lines = []
    if 'requestBody' in op:
        rb, rb_doc = deref(op['requestBody'], doc_name)
        schema = rb['content']['application/json']['schema']
        headers.append({'key': 'Content-Type', 'value': 'application/json'})
        body = {'mode': 'raw', 'raw': json.dumps(example(schema, rb_doc), indent=2), 'options': {'raw': {'language': 'json'}}}
        body_lines = describe_schema(schema, rb_doc)
    if write:
        headers.append({'key': 'Idempotency-Key', 'value': '{{$guid}}',
                        'description': 'A new key per write; resend the same key to retry safely (the server replays the stored answer).'})
    responses = []
    for code, response in op.get('responses', {}).items():
        resp, resp_doc = deref(response, doc_name)
        responses.append(f"- **{code}** — {resp.get('description', '')}")
    rules = op.get('x-theme5-rules', [])
    security = ', '.join(next(iter(s)) for s in op.get('security', [])) or 'none (no bearer token)'
    description = '\n'.join([
        f"**{op.get('summary', '')}**", '', op.get('description', ''), '',
        f"- operationId: `{op['operationId']}`", f"- Authentication: {security}",
        f"- Write: {'yes (send an Idempotency-Key)' if write else 'no'}",
        *( [f"- Rules: {', '.join('`' + r + '`' for r in rules)}"] if rules else []),
        *( ['', '**Path parameters**'] + [f"- `{p['name']}` — {p.get('description', '')} {json.dumps(p.get('schema', {}))}" for p in path_params] if path_params else []),
        *( ['', '**Query parameters**'] + [f"- `{p['name']}` ({'required' if p.get('required') else 'optional'}) — {p.get('description', '')} {json.dumps(p.get('schema', {}))}" for p in query] if query else []),
        *( ['', '**Cookie**'] + [f"- `{p['name']}` — {p.get('description', '')}" for p in cookie_params] if cookie_params else []),
        *( ['', '**Body fields**'] + body_lines if body_lines else []),
        '', '**Responses**', *responses,
    ])
    url = {'raw': '{{baseUrl}}/' + '/'.join(segments) + ('?' + '&'.join(f"{p['name']}={QUERY_DEFAULTS.get(p['name'], '')}" for p in query) if query else ''),
           'host': ['{{baseUrl}}'], 'path': segments}
    if path_params:
        url['variable'] = [{'key': p['name'], 'value': PATH_DEFAULTS.get(p['name'], ''), 'description': p.get('description', '')} for p in path_params]
    if query:
        url['query'] = [{'key': p['name'], 'value': QUERY_DEFAULTS.get(p['name'], ''), 'description': p.get('description', '')} for p in query]
    request = {'method': method.upper(), 'header': headers, 'url': url, 'description': description}
    if body:
        request['body'] = body
    if not secured:
        request['auth'] = {'type': 'noauth'}
    item = {'name': op.get('summary') or op['operationId'], 'request': request}
    events = []
    if op['operationId'] in ('signIn', 'refreshSession'):
        events.append({'listen': 'test', 'script': {'type': 'text/javascript', 'exec': [
            'if (pm.response.code === 200) {',
            '  const grant = pm.response.json();',
            "  pm.environment.set('accessToken', grant.accessToken);",
            "  pm.environment.set('staffId', grant.staff && grant.staff.staffId);",
            '}']}})
    if op['operationId'] == 'signOut':
        events.append({'listen': 'test', 'script': {'type': 'text/javascript', 'exec': ["pm.environment.unset('accessToken');"]}})
    if events:
        item['event'] = events
    return item


OVERVIEW = (HERE / 'COLLECTION_OVERVIEW.md').read_text()


def main():
    folders = []
    for doc_name, folder in FOLDERS:
        doc = DOCS[doc_name]
        items = [request_item(path, method, op, doc_name) for path, ops in doc['paths'].items() for method, op in ops.items()]
        folders.append({'name': folder, 'description': doc['info'].get('description', ''), 'item': items})
    collection = {
        'info': {'name': 'Rikskampen X — Theme 5 API', 'description': OVERVIEW,
                 'schema': 'https://schema.getpostman.com/json/collection/v2.1.0/collection.json'},
        'auth': {'type': 'bearer', 'bearer': [{'key': 'token', 'value': '{{accessToken}}', 'type': 'string'}]},
        'item': folders,
    }
    (HERE / 'Theme5-API.postman_collection.json').write_text(json.dumps(collection, indent=2, ensure_ascii=False) + '\n')
    values = {'baseUrl': 'http://localhost:8080', 'accessToken': '', 'staffId': '', 'clientId': 'u1', 'week': '1', 'dayIndex': '4',
              'mealType': 'Breakfast', 'recordId': 'activity-1', 'suggestionId': 'suggestion-1', 'date': '2026-10-02',
              'fromDate': '2026-09-28', 'toDate': '2026-10-04'}
    environment = {'name': 'Theme 5 — local', 'values': [{'key': k, 'value': v, 'enabled': True, **({'type': 'secret'} if k == 'accessToken' else {})}
                                                       for k, v in values.items()]}
    (HERE / 'Theme5-local.postman_environment.json').write_text(json.dumps(environment, indent=2) + '\n')
    count = sum(len(f['item']) for f in folders)
    print(json.dumps({'folders': len(folders), 'requests': count}))


if __name__ == '__main__':
    main()
