#
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements.  See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
# The ASF licenses this file to You under the Apache License, Version 2.0
# (the "License"); you may not use this file except in compliance with
# the License.  You may obtain a copy of the License at
#
#    http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

"""An example that reads New York City for hire vehicle trips.

An example that reads New York City for hire vehicle trips data from Google
Cloud Storage and calculates various statistics including the passenger's
price per trip, price per mile, and price per minute.

Data originally downloaded as parquet files from the New York City
government
`website <https://www.nyc.gov/site/tlc/about/tlc-trip-record-data.page>`_
before being converted to avro format. Here is the
`documentation
<https://www.nyc.gov/assets/tlc/downloads/pdf/data_dictionary_trip_records_hvfhs.pdf>`_
for the data format and schema.

To execute this pipeline locally using the DirectRunner, specify an
output prefix on GCS:::

  --output gs://YOUR_OUTPUT_PREFIX

To execute this pipeline using the Google Cloud Dataflow service, specify
pipeline configuration in addition to the above:::

  --job_name NAME_FOR_YOUR_JOB
  --project YOUR_PROJECT_ID
  --region GCE_REGION
  --staging_location gs://YOUR_STAGING_DIRECTORY
  --temp_location gs://YOUR_TEMPORARY_DIRECTORY
  --runner DataflowRunner

The default input is
``gs://apache-beam-samples/nyc_trip/avro/fhvhv_tripdata_2023-02.avro`` and
takes about 15 minutes with 6 workers running on Dataflow. The default input
can be overridden with --input argument. More data can be accessed at
``gs://apache-beam-samples/nyc_trip/avro/*``.

Additionally, the original parquet files can be found at
``gs://apache-beam-samples/nyc_trip/parquet/*``.
"""

# pytype: skip-file

import argparse
import datetime
import logging

import pytz

import apache_beam as beam
from apache_beam.io import ReadFromAvro
from apache_beam.io import WriteToAvro
from apache_beam.options.pipeline_options import PipelineOptions
from apache_beam.options.pipeline_options import SetupOptions

SCHEMA = {
    'fields': [
        {
            'name': 'service', 'type': 'string'
        },
        {
            'name': 'day', 'type': 'string'
        },
        {
            'name': 'total_price', 'type': 'double'
        },
        {
            'name': 'total_driver_pay', 'type': 'double'
        },
        {
            'name': 'total_trip_miles', 'type': 'double'
        },
        {
            'name': 'total_trip_minutes', 'type': 'double'
        },
        {
            'name': 'total_number_of_trips', 'type': 'long'
        },
        {
            'name': 'price_per_trip', 'type': 'double'
        },
        {
            'name': 'price_per_mile', 'type': 'double'
        },
        {
            'name': 'price_per_minute', 'type': 'double'
        },
        {
            'name': 'driver_pay_per_trip', 'type': 'double'
        },
        {
            'name': 'driver_pay_per_mile', 'type': 'double'
        },
        {
            'name': 'driver_pay_per_minute', 'type': 'double'
        },
        {
            'name': 'miles_per_hour', 'type': 'double'
        },
    ],
    'name': 'nyc_trip_prices',
    'type': 'record',
}


class CreateKeyWithServiceAndDay(beam.DoFn):
  """Creates a key, value group.

  The key is the combination of the HVFHS Code converted to the ride-sharing
  company name and the day of the week of the ride. The value is the original
  record dictionary object.
  """
  def process(self, record: dict):
    options = {
        'HV0002': 'Juno', 'HV0003': 'Uber', 'HV0004': 'Via', 'HV0005': 'Lyft'
    }
    service = options.get(record['hvfhs_license_num'])
    if service:
      timestamp = None
      for k in ('request_datetime',
                'on_scene_datetime',
                'pickup_datetime',
                'dropoff_datetime'):
        timestamp = timestamp or record[k]
        if timestamp:
          break

      day_of_the_week = datetime.datetime.fromtimestamp(
          timestamp / 1000.0,
          tz=pytz.timezone('America/New_York')).strftime('%a')

      yield (service, day_of_the_week), record


class CalculatePricePerAttribute(beam.CombineFn):
  """Calculates the price for attribute.

  Calculates the total driver pay, price, miles, minutes, and trips per for
  hire vehicle service. And calculates the price per mile, minute, and trip
  for both the driver and passenger.
  """
  def create_accumulator(self):
    total_price = 0.0
    total_driver_pay = 0.0
    total_trip_miles = 0.0
    total_trip_time = 0.0
    total_number_of_trips = 0
    accumulator = (
        total_price,
        total_driver_pay,
        total_trip_miles,
        total_trip_time,
        total_number_of_trips,
    )
    return accumulator

  def add_input(self, accumulator, record):
    (
        total_price,
        total_driver_pay,
        total_trip_miles,
        total_trip_time,
        total_number_of_trips,
    ) = accumulator
    return (
        total_price + sum(
            record[name] for name in (
                'base_passenger_fare',
                'tolls',
                'bcf',
                'sales_tax',
                'congestion_surcharge',
                'airport_fee',
                'tips') if record[name] is not None),
        total_driver_pay + record['driver_pay'] + record['tips'],
        total_trip_miles + record['trip_miles'],
        total_trip_time + record['trip_time'],
        total_number_of_trips + 1,
    )

  def merge_accumulators(self, accumulators):
    # calculates the sum for each item in the accumulator
    return tuple(sum(item) for item in zip(*accumulators))

  def extract_output(self, accumulator):
    (
        total_price,
        total_driver_pay,
        total_trip_miles,
        total_trip_time,
        total_number_of_trips,
    ) = accumulator
    total_trip_minutes = total_trip_time / 60
    return {
        'total_driver_pay': total_driver_pay,
        'total_price': total_price,
        'total_trip_miles': total_trip_miles,
        'total_trip_minutes': total_trip_minutes,
        'total_number_of_trips': total_number_of_trips,
        'price_per_trip': total_price / total_number_of_trips,
        'price_per_mile': total_price / total_trip_miles,
        'price_per_minute': total_price / total_trip_minutes,
        'driver_pay_per_trip': total_driver_pay / total_number_of_trips,
        'driver_pay_per_mile': total_driver_pay / total_trip_miles,
        'driver_pay_per_minute': total_driver_pay / total_trip_minutes,
        'miles_per_hour': total_trip_miles / (total_trip_minutes / 60),
    }


def flatten_group(element):
  """Flattens the key, value pair to a single record dictionary."""
  key, record = element
  service, day_of_the_week = key
  record['service'] = service
  record['day'] = day_of_the_week
  return record


def run(argv=None):
  """Runs the New York City trips pipeline.

  Args:
    argv: Pipeline options as a list of arguments.
  """
  parser = argparse.ArgumentParser()
  parser.add_argument(
      '--input',
      dest='input',
      default=
      'gs://apache-beam-samples/nyc_trip/avro/fhvhv_tripdata_2023-02.avro',
      help='Input file of NYC FHV data to process. Larger dataset can be found '
      'here: gs://apache-beam-samples/nyc_trip/avro/*',
  )
  parser.add_argument(
      '--output',
      dest='output',
      help='Output file to write results to.',
      required=True,
  )
  known_args, pipeline_args = parser.parse_known_args(argv)

  # We use the save_main_session option because one or more DoFn's in this
  # workflow rely on global context (e.g., a module imported at module level).
  pipeline_options = PipelineOptions(pipeline_args)
  pipeline_options.view_as(SetupOptions).save_main_session = True

  with beam.Pipeline(options=pipeline_options) as p:
    (  # pylint: disable=expression-not-assigned
        p
        | ReadFromAvro(known_args.input)
        # filter records that have keys that are not None
        # and at least one timestamp that is not None
        | beam.Filter(
            lambda record: all(
                record[k] is not None for k in (
                    'hvfhs_license_num',
                    'trip_miles',
                    'trip_time',
                    'base_passenger_fare',
                    'tips',
                    'driver_pay')) and any(
                        record[k] is not None for k in (
                            'request_datetime',
                            'on_scene_datetime',
                            'pickup_datetime',
                            'dropoff_datetime')))
        | beam.ParDo(CreateKeyWithServiceAndDay())
        | beam.CombinePerKey(CalculatePricePerAttribute())
        | beam.Map(flatten_group)
        | WriteToAvro(known_args.output, SCHEMA, file_name_suffix='.avro'))


if __name__ == '__main__':
  logging.getLogger().setLevel(logging.INFO)
  run()
