#!/usr/bin/env python3
"""Import POI and category data from the attractions.io records bundle."""

import io
import json
import logging
import os
import sys
import uuid
import zipfile
from datetime import datetime, timezone

import psycopg2
import requests
from psycopg2.extras import execute_values

API_BASE = 'https://api.attractions.io/v1'
API_KEY = '72f0ea9e-d196-508a-bee8-cce62c3228c7'  # Knoebels app id (BuildConfig.API_KEY)
APP_VERSION = '1.3.2'   # bump if a newer xapk reveals drift
APP_BUILD = 70
NATIVE_VERSION = '1.0.129+6c346f2'

DB_PASSWORD = os.environ.get('DB_PASSWORD')
if not DB_PASSWORD:
    sys.stderr.write("ERROR: DB_PASSWORD env var is required\n")
    sys.exit(1)

db_params = {
    'dbname': 'gp0',
    'user': os.environ.get('DB_USER', 'kuhnobowls'),
    'password': DB_PASSWORD,
    'host': os.environ.get('DB_HOST', '192.168.88.9'),
    'port': os.environ.get('DB_PORT', '5432'),
    'options': '-c search_path=knoebels',
}

CATEGORY_COLUMNS = ('_id', 'name', 'parent')

POI_COLUMNS = (
    '_id', 'name', 'abbreviated_name', 'summary', 'keywords', 'default_image',
    'location', 'featured', 'wayfinding_enabled', 'visible_on_map', 'category',
    'parent', 'menu_url', 'minimum_height_requirement',
    'minimum_unaccompanied_height_requirement', 'maximum_height_requirement',
    'minimum_age_requirement', 'minimum_unaccompanied_age_requirement',
    'maximum_age_requirement', 'restriction_summary',
)


def _occasio_headers(installation_token=None):
    auth = f'Attractions-Io api-key="{API_KEY}"'
    if installation_token:
        auth += f', installation-token="{installation_token}"'
    return {
        'Authorization': auth,
        'X-Idempotency-Key': str(uuid.uuid4()),
        'Date': datetime.now(timezone.utc).strftime('%Y-%m-%dT%H:%M:%SZ'),
        'Occasio-Platform': 'Android',
        'Occasio-Platform-Version': '14',
        'Occasio-App-Version': APP_VERSION,
        'Occasio-App-Build': str(APP_BUILD),
        'Occasio-Native-Version': NATIVE_VERSION,
    }


def fetch_records_bundle():
    """Two-step Occasio data fetch: register an installation, then pull the data zip."""
    install_resp = requests.post(
        f'{API_BASE}/installation',
        headers=_occasio_headers(),
        files={
            'device_identifier': (None, '123'),
            'user_identifier': (None, str(uuid.uuid4())),
            'app_build': (None, str(APP_BUILD)),
            'app_version': (None, APP_VERSION),
        },
    )
    install_resp.raise_for_status()
    token = install_resp.json()['token']

    data_resp = requests.get(
        f'{API_BASE}/data',
        headers=_occasio_headers(installation_token=token),
    )
    data_resp.raise_for_status()

    with zipfile.ZipFile(io.BytesIO(data_resp.content)) as zf:
        with zf.open('records.json') as f:
            return json.load(f)


def _location_to_point(loc):
    # Location arrives as 'lat,lon'; Postgres point input wants '(x,y)'.
    if not loc:
        return None
    return f'({loc})'


def _category_row(cat):
    return (cat['_id'], cat['Name'], cat.get('Parent'))


def _poi_row(poi):
    return (
        poi['_id'],
        poi.get('Name'),
        poi.get('AbbreviatedName'),
        poi.get('Summary'),
        poi.get('Keywords'),
        poi.get('DefaultImage'),
        _location_to_point(poi.get('Location')),
        poi.get('Featured'),
        poi.get('WayfindingEnabled'),
        poi.get('VisibleOnMap'),
        poi.get('Category'),
        poi.get('Parent'),
        poi.get('MenuURL'),
        poi.get('MinimumHeightRequirement'),
        poi.get('MinimumUnaccompaniedHeightRequirement'),
        poi.get('MaximumHeightRequirement'),
        poi.get('MinimumAgeRequirement'),
        poi.get('MinimumUnaccompaniedAgeRequirement'),
        poi.get('MaximumAgeRequirement'),
        poi.get('RestrictionSummary'),
    )


def upsert_rows(conn, table, columns, rows, *, location_column=None):
    if not rows:
        return 0
    cols_sql = ', '.join(f'"{c}"' for c in columns)
    update_sql = ', '.join(f'"{c}" = EXCLUDED."{c}"' for c in columns if c != '_id')
    # Force the 'point' cast on the location column so multi-row VALUES doesn't
    # fall back to inferring the type as text.
    placeholders = ', '.join(
        '%s::point' if c == location_column else '%s' for c in columns
    )
    template = f'({placeholders})'
    query = (
        f'INSERT INTO knoebels."{table}" ({cols_sql}) VALUES %s '
        f'ON CONFLICT (_id) DO UPDATE SET {update_sql}'
    )
    with conn.cursor() as cur:
        execute_values(cur, query, rows, template=template)
    return len(rows)


def main():
    logging.basicConfig(format='%(asctime)s %(levelname)s %(message)s', level=logging.INFO)

    logging.info('Fetching records bundle from attractions.io...')
    data = fetch_records_bundle()
    logging.info('Got %d Items, %d Categories', len(data['Item']), len(data['Category']))

    cats = [_category_row(c) for c in data['Category']]
    pois = [_poi_row(p) for p in data['Item']]

    with psycopg2.connect(**db_params) as conn:
        n_cats = upsert_rows(conn, 'LZ_attractions_io_categories', CATEGORY_COLUMNS, cats)
        n_pois = upsert_rows(conn, 'LZ_attractions_io_poi', POI_COLUMNS, pois,
                             location_column='location')
        conn.commit()
    logging.info('Upserted %d categories, %d POIs', n_cats, n_pois)


if __name__ == '__main__':
    main()
