import binascii
import logging
from datetime import datetime, timedelta
from json import dumps
from typing import Any, Dict, List

import requests
from celery import shared_task
from django.core.cache import cache
from pytz import utc

from nephrogo import settings
from vintra import models, transit_realtime
from vintra.models import Agency
from vintra.utils import parse_transport_positions

logger = logging.getLogger(__name__)


class IVPKClient:
    _auth_token_url = 'https://put-test.data.gov.lt/auth/token'
    _agency_url = 'https://put-test.data.gov.lt/datasets/gov/sumin/vintra/Agency'
    _vehicle_url = 'https://put-test.data.gov.lt/datasets/gov/sumin/vintra/Vehicle'
    _trip_url = 'https://put-test.data.gov.lt/datasets/gov/sumin/vintra/Trip'
    _positions_url = 'https://put-test.data.gov.lt/datasets/gov/sumin/vintra/Position'

    _ivpk_access_token_key = 'IVPK_ACCESS_TOKEN'

    def _get_api_call_headers(self):
        access_token = None

        if access_token is None:
            access_token_response = self._generate_access_token_response()
            access_token = access_token_response['access_token']

            cache.set(
                self._ivpk_access_token_key,
                access_token,
                access_token_response['expires_in'],
            )

        return {'Authorization': 'Bearer ' + access_token}

    def _generate_access_token_response(self):
        client_id = settings.IVPK_CLIENT_ID
        client_secret = settings.IVPK_CLIENT_SECRET
        scopes = [
            'spinta_set_meta_fields',
            'spinta_getall',
            'spinta_getone',
            'spinta_search',
            'spinta_changes',
            'spinta_datasets_gov_sumin_insert',
            'spinta_datasets_gov_sumin_upsert',
            'spinta_datasets_gov_sumin_update',
            'spinta_datasets_gov_sumin_patch',
            'spinta_datasets_gov_sumin_delete'
        ]

        access_token_response = requests.post(
            self._auth_token_url,
            data={'grant_type': 'client_credentials', 'scope': ' '.join(scopes)},
            auth=(client_id, client_secret)
        )

        access_token_response.raise_for_status()
        return access_token_response.json()

    def delete(self, url: str):
        r = requests.delete(
            url,
            headers=self._get_api_call_headers(),
            timeout=(3, 10),
        )

        if r.status_code == 404:
            return False

        # r.raise_for_status()

        return True

    def _save_batch(self, url: str, json: List[Any]) -> list[str]:
        if not json:
            return []

        data_to_post = '\n'.join(dumps(d) for d in json) + "\n"

        headers = {
            **self._get_api_call_headers(),
            "Content-Type": "application/x-ndjson",
        }

        r = requests.post(
            url,
            headers=headers,
            timeout=(3, 10),
            data=data_to_post,
        )

        try:
            r.raise_for_status()
        except Exception as e:
            logging.warning(e, exc_info=True, extra={'text': r.text})
            raise e

        return list(d['_id'] for d in r.json()['_data'])

    def _save_agency(self, agency: Agency):
        return self._save_batch(
            url=self._agency_url,
            json=[
                {
                    "_op": "upsert",
                    "_where": f"agency_id='{agency.id}'",
                    "agency_id": str(agency.id),
                    "name": agency.name,
                }
            ]
        )[0]

    def _save_vehicles(self, agency_id: str, vehicles: list[transit_realtime.VehicleDescriptor]) -> list[str]:
        save_vehicles_request = [{
            "_op": "upsert",
            "_type": 'datasets/gov/sumin/vintra/Vehicle',
            "_where": f"(vehicle_id='{v.id}')&(agency._id='{agency_id}')",
            "agency": {
                "_id": agency_id,
            },
            "vehicle_id": v.id,
            "plate_number": v.license_plate,
        } for v in vehicles]

        return self._save_batch(
            self._vehicle_url,
            save_vehicles_request,
        )

    def _save_trips(self, agency_id: str, trip: list[transit_realtime.TripDescriptor]) -> list[str]:
        save_trips_request = [{
            "_op": "upsert",
            "_type": "datasets/gov/sumin/vintra/Trip",
            "_where": f"(agency._id='{agency_id}')&(trip_id='{t.trip_id}')",
            "agency": {
                "_id": agency_id,
            },
            "trip_id": t.trip_id,
        } for t in trip]

        return self._save_batch('https://put-test.data.gov.lt/datasets/gov/sumin/vintra/Trip', save_trips_request)

    def save_positions(
            self,
            agency: Agency,
            vehicle_positions: list[transit_realtime.VehiclePosition],
            trip_updates: list[transit_realtime.TripUpdate],
    ):
        agency_id = self._save_agency(agency)
        vehicles = list([p.vehicle for p in vehicle_positions])
        vehicle_ids = self._save_vehicles(agency_id, vehicles)

        trips = list([p.trip for p in vehicle_positions])
        trip_ids = self._save_trips(agency_id, trips)

        delay_lookup = {
            t.trip.trip_id: t.stop_time_update[0].departure.delay if len(t.stop_time_update) > 0 else None
            for t in trip_updates
        }

        save_transport_positions_request = [{
            "_type": "datasets/gov/sumin/vintra/Position",
            "_op": "insert",
            "vehicle": {
                "_id": vehicle_ids[i],
            },
            "trip": {
                "_id": trip_ids[i],
            },
            "bearing": t.position.bearing,
            "speed": round(t.position.speed, 3),
            "position": f'POINT({t.position.latitude:.6f} {t.position.longitude:.6f})',
            "delay": delay_lookup.get(t.trip.trip_id, None) if t.trip.trip_id else None,
            "timestamp": str(datetime.fromtimestamp(t.timestamp, tz=utc)),
        } for i, t in enumerate(vehicle_positions)]

        return self._save_batch(self._positions_url, save_transport_positions_request)


@shared_task(soft_time_limit=15, time_limit=30)
def fetch_gtfs_realtime(agency_id: int):
    agency = Agency.objects.get(pk=agency_id)

    if (agency.last_update + timedelta(seconds=5)).replace(tzinfo=utc) > datetime.now(tz=utc):
        return 0

    agency.update_last_update_to_now()

    r = requests.get(agency.protobuf_realtime_url, timeout=(3, 10), verify=False)
    r.raise_for_status()

    agency.update_last_update_to_now()

    content_base64 = binascii.b2a_base64(r.content).decode('utf8')
    save_gtfs_realtime_to_ivpk.delay(agency_id, content_base64)

    return 1


@shared_task(soft_time_limit=30, time_limit=60)
def save_gtfs_realtime_to_ivpk(agency_id: int, pb_content_base64: str):
    agency = Agency.objects.get(pk=agency_id)
    pb_content = binascii.a2b_base64(pb_content_base64)

    entity = transit_realtime.FeedMessage.FromString(pb_content)

    ivpk_client = IVPKClient()

    vehicles = [v.vehicle for v in entity.entity if v.vehicle.vehicle.id]
    trip_updates = [v.trip_update for v in entity.entity if v.trip_update.trip.trip_id]

    return len(ivpk_client.save_positions(agency, vehicles, trip_updates))


@shared_task(soft_time_limit=20, time_limit=30)
def fetch_vehicle_positions(agency_id):
    agency = Agency.objects.get(pk=agency_id)
    trip_id_field_name = agency.trip_id_field_name or 'Reisas'

    if (agency.last_update + timedelta(seconds=20)).replace(tzinfo=utc) > datetime.now(tz=utc):
        return 0

    r = requests.get(agency.realtime_url, timeout=(2, 5), verify=False)

    r.raise_for_status()

    transport_positions = list(parse_transport_positions(r.text, trip_id_field_name=trip_id_field_name))

    for transport_position in transport_positions:
        vehicle, _ = models.VehicleDescriptor.objects.get_or_create(
            agency=agency,
            vehicle_id=transport_position.vehicle_id,
            plate_number=transport_position.plate_number,
        ) if transport_position.vehicle_id else (None, None)

        trip, _ = models.TripDescriptor.objects.get_or_create(
            agency=agency,
            trip_id=transport_position.trip_id,
            route=transport_position.route,
            start_time=transport_position.start_time,
        )

        position, _ = models.Position.objects.get_or_create(
            latitude=transport_position.latitude,
            longitude=transport_position.longitude,
            bearing=transport_position.bearing,
            speed=transport_position.speed,
        )

        models.VehiclePosition.objects.update_or_create(
            trip=trip,
            vehicle=vehicle,
            position=position,
            delay=transport_position.delay,
        )

    return len(transport_positions)


@shared_task(soft_time_limit=10)
def fetch_vehicle_positions_for_all_agencies():
    for agency in Agency.objects.filter(protobuf_realtime_url__isnull=False):
        fetch_gtfs_realtime.delay(agency.id)

    # for agency in Agency.objects.all():
    #     fetch_vehicle_positions.delay(agency.id)

import binascii
import logging
from datetime import datetime, timedelta
from json import dumps
from typing import Any, Dict, List

import requests
from celery import shared_task
from django.core.cache import cache
from pytz import utc

from nephrogo import settings
from vintra import models, transit_realtime
from vintra.models import Agency
from vintra.utils import parse_transport_positions

logger = logging.getLogger(__name__)


class IVPKClient:
    _auth_token_url = 'https://put-test.data.gov.lt/auth/token'
    _agency_url = 'https://put-test.data.gov.lt/datasets/gov/sumin/vintra/Agency'
    _vehicle_url = 'https://put-test.data.gov.lt/datasets/gov/sumin/vintra/Vehicle'
    _trip_url = 'https://put-test.data.gov.lt/datasets/gov/sumin/vintra/Trip'
    _positions_url = 'https://put-test.data.gov.lt/datasets/gov/sumin/vintra/Position'

    _ivpk_access_token_key = 'IVPK_ACCESS_TOKEN'

    def _get_api_call_headers(self):
        access_token = None

        if access_token is None:
            access_token_response = self._generate_access_token_response()
            access_token = access_token_response['access_token']

            cache.set(
                self._ivpk_access_token_key,
                access_token,
                access_token_response['expires_in'],
            )

        return {'Authorization': 'Bearer ' + access_token}

    def _generate_access_token_response(self):
        client_id = settings.IVPK_CLIENT_ID
        client_secret = settings.IVPK_CLIENT_SECRET
        scopes = [
            'spinta_set_meta_fields',
            'spinta_getall',
            'spinta_getone',
            'spinta_search',
            'spinta_changes',
            'spinta_datasets_gov_sumin_insert',
            'spinta_datasets_gov_sumin_upsert',
            'spinta_datasets_gov_sumin_update',
            'spinta_datasets_gov_sumin_patch',
            'spinta_datasets_gov_sumin_delete'
        ]

        access_token_response = requests.post(
            self._auth_token_url,
            data={'grant_type': 'client_credentials', 'scope': ' '.join(scopes)},
            auth=(client_id, client_secret)
        )

        access_token_response.raise_for_status()
        return access_token_response.json()

    def delete(self, url: str):
        r = requests.delete(
            url,
            headers=self._get_api_call_headers(),
            timeout=(3, 10),
        )

        if r.status_code == 404:
            return False

        # r.raise_for_status()

        return True

    def _save_batch(self, url: str, json: List[Any]) -> list[str]:
        if not json:
            return []

        data_to_post = '\n'.join(dumps(d) for d in json) + "\n"

        headers = {
            **self._get_api_call_headers(),
            "Content-Type": "application/x-ndjson",
        }

        r = requests.post(
            url,
            headers=headers,
            timeout=(3, 10),
            data=data_to_post,
        )

        try:
            r.raise_for_status()
        except Exception as e:
            logging.warning(e, exc_info=True, extra={'text': r.text})
            raise e

        return list(d['_id'] for d in r.json()['_data'])

    def _save_agency(self, agency: Agency):
        return self._save_batch(
            url=self._agency_url,
            json=[
                {
                    "_op": "upsert",
                    "_where": f"agency_id='{agency.id}'",
                    "agency_id": str(agency.id),
                    "name": agency.name,
                }
            ]
        )[0]

    def _save_vehicles(self, agency_id: str, vehicles: list[transit_realtime.VehicleDescriptor]) -> list[str]:
        save_vehicles_request = [{
            "_op": "upsert",
            "_type": 'datasets/gov/sumin/vintra/Vehicle',
            "_where": f"(vehicle_id='{v.id}')&(agency._id='{agency_id}')",
            "agency": {
                "_id": agency_id,
            },
            "vehicle_id": v.id,
            "plate_number": v.license_plate,
        } for v in vehicles]

        return self._save_batch(
            self._vehicle_url,
            save_vehicles_request,
        )

    def _save_trips(self, agency_id: str, trip: list[transit_realtime.TripDescriptor]) -> list[str]:
        save_trips_request = [{
            "_op": "upsert",
            "_type": "datasets/gov/sumin/vintra/Trip",
            "_where": f"(agency._id='{agency_id}')&(trip_id='{t.trip_id}')",
            "agency": {
                "_id": agency_id,
            },
            "trip_id": t.trip_id,
        } for t in trip]

        return self._save_batch('https://put-test.data.gov.lt/datasets/gov/sumin/vintra/Trip', save_trips_request)

    def save_positions(
            self,
            agency: Agency,
            vehicle_positions: list[transit_realtime.VehiclePosition],
            trip_updates: list[transit_realtime.TripUpdate],
    ):
        agency_id = self._save_agency(agency)
        vehicles = list([p.vehicle for p in vehicle_positions])
        vehicle_ids = self._save_vehicles(agency_id, vehicles)

        trips = list([p.trip for p in vehicle_positions])
        trip_ids = self._save_trips(agency_id, trips)

        delay_lookup = {
            t.trip.trip_id: t.stop_time_update[0].departure.delay if len(t.stop_time_update) > 0 else None
            for t in trip_updates
        }

        save_transport_positions_request = [{
            "_type": "datasets/gov/sumin/vintra/Position",
            "_op": "insert",
            "vehicle": {
                "_id": vehicle_ids[i],
            },
            "trip": {
                "_id": trip_ids[i],
            },
            "bearing": t.position.bearing,
            "speed": round(t.position.speed, 3),
            "position": f'POINT({t.position.latitude:.6f} {t.position.longitude:.6f})',
            "delay": delay_lookup.get(t.trip.trip_id, None) if t.trip.trip_id else None,
            "timestamp": str(datetime.fromtimestamp(t.timestamp, tz=utc)),
        } for i, t in enumerate(vehicle_positions)]

        return self._save_batch(self._positions_url, save_transport_positions_request)


@shared_task(soft_time_limit=15, time_limit=30)
def fetch_gtfs_realtime(agency_id: int):
    agency = Agency.objects.get(pk=agency_id)

    if (agency.last_update + timedelta(seconds=5)).replace(tzinfo=utc) > datetime.now(tz=utc):
        return 0

    agency.update_last_update_to_now()

    r = requests.get(agency.protobuf_realtime_url, timeout=(3, 10), verify=False)
    r.raise_for_status()

    agency.update_last_update_to_now()

    content_base64 = binascii.b2a_base64(r.content).decode('utf8')
    save_gtfs_realtime_to_ivpk.delay(agency_id, content_base64)

    return 1


@shared_task(soft_time_limit=30, time_limit=60)
def save_gtfs_realtime_to_ivpk(agency_id: int, pb_content_base64: str):
    agency = Agency.objects.get(pk=agency_id)
    pb_content = binascii.a2b_base64(pb_content_base64)

    entity = transit_realtime.FeedMessage.FromString(pb_content)

    ivpk_client = IVPKClient()

    vehicles = [v.vehicle for v in entity.entity if v.vehicle.vehicle.id]
    trip_updates = [v.trip_update for v in entity.entity if v.trip_update.trip.trip_id]

    return len(ivpk_client.save_positions(agency, vehicles, trip_updates))


@shared_task(soft_time_limit=20, time_limit=30)
def fetch_vehicle_positions(agency_id):
    agency = Agency.objects.get(pk=agency_id)
    trip_id_field_name = agency.trip_id_field_name or 'Reisas'

    if (agency.last_update + timedelta(seconds=20)).replace(tzinfo=utc) > datetime.now(tz=utc):
        return 0

    r = requests.get(agency.realtime_url, timeout=(2, 5), verify=False)

    r.raise_for_status()

    transport_positions = list(parse_transport_positions(r.text, trip_id_field_name=trip_id_field_name))

    for transport_position in transport_positions:
        vehicle, _ = models.VehicleDescriptor.objects.get_or_create(
            agency=agency,
            vehicle_id=transport_position.vehicle_id,
            plate_number=transport_position.plate_number,
        ) if transport_position.vehicle_id else (None, None)

        trip, _ = models.TripDescriptor.objects.get_or_create(
            agency=agency,
            trip_id=transport_position.trip_id,
            route=transport_position.route,
            start_time=transport_position.start_time,
        )

        position, _ = models.Position.objects.get_or_create(
            latitude=transport_position.latitude,
            longitude=transport_position.longitude,
            bearing=transport_position.bearing,
            speed=transport_position.speed,
        )

        models.VehiclePosition.objects.update_or_create(
            trip=trip,
            vehicle=vehicle,
            position=position,
            delay=transport_position.delay,
        )

    return len(transport_positions)


@shared_task(soft_time_limit=10)
def fetch_vehicle_positions_for_all_agencies():
    for agency in Agency.objects.filter(protobuf_realtime_url__isnull=False):
        fetch_gtfs_realtime.delay(agency.id)

    # for agency in Agency.objects.all():
    #     fetch_vehicle_positions.delay(agency.id)

# r.raise_for_status() # for agency in Agency.objects.all(): #     fetch_vehicle_positions.delay(agency.id)