#![allow(dead_code)]

use std::path::PathBuf;

use anyhow::{anyhow, Result};
use chrono::naive::NaiveDate;
use chrono::Datelike;
use chrono::Local;
use std::cmp::min;

use clap::{Args, Parser, Subcommand};

use async_trait::async_trait;
use google_walletobjects1::api::{
    FlightClass, FlightObject, FlightclasMethods, FlightobjectMethods,
};
use google_walletobjects1::oauth2::authenticator::{
    Authenticator, DefaultHyperClient, HyperClientBuilder,
};
use google_walletobjects1::oauth2::{read_service_account_key, ServiceAccountAuthenticator};
use jwt_simple::algorithms::{RS256KeyPair, RSAKeyPairLike};
use jwt_simple::claims::JWTClaims;
use strum::IntoEnumIterator;
use strum_macros::EnumIter;

use serde::{Deserialize, Serialize};

use phf::phf_map;
use tokio::try_join;

#[derive(Parser)]
#[command(author, version, about, long_about = None, propagate_version = true, subcommand_required = true, arg_required_else_help = true)]
struct Cli {
    #[arg(short, long)]
    service_account_file: Option<PathBuf>,

    #[arg(short, long, default_value = "3388000000022186420")]
    issuer_id: String,

    #[command(subcommand)]
    command: Option<Commands>,
}

#[derive(Subcommand)]
enum Commands {
    /// gets an existing pass
    GetPass(GetPass),

    /// gets an existing class
    GetClass(GetClass),

    /// uploads a class/pass pair, creating it if necessary. A Google Wallet Add link is printed.
    Upload(Upload),

    /// generates a class/pass pair from an image, uploads it, and then prints a Google Wallet Add
    /// link.
    UploadScan(UploadScan),

    /// reads the aztec code out of an image
    Scan {
        /// Path to the image to read.
        image: PathBuf,
    },
}

#[derive(Args)]
struct GetClass {
    #[arg(short, long)]
    object_type: String,

    #[arg(short, long)]
    class_id: String,
}

#[derive(Args)]
struct GetPass {
    #[arg(short, long)]
    object_type: String,

    #[arg(short, long)]
    pass_id: String,
}

#[derive(Args)]
struct Upload {
    #[arg(short, long)]
    class_json: PathBuf,

    #[arg(short, long)]
    pass_json: PathBuf,
}

#[derive(Args)]
struct UploadScan {
    image: PathBuf,
}

struct AirlineDefinition<'a> {
    iata_code: &'a str,
    name: &'a str,
    boarding_policy: &'a str,
    seat_class_policy: &'a str,

    boarding_pass_background_colour: &'a str,

    frequent_flyer_program_name: Option<&'a str>,

    logo_url: &'a str,
    alliance_logo_url: Option<&'a str>,
    hero_image_logo_url: Option<&'a str>,
    boarding_privilege_logo_url: Option<&'a str>,
}

const TSA_PRECHECK_LOGO: &str = "https://p.lukegb.com/raw/MiserablyDirectPiglet.jpg";

static AIRLINE_DATA: phf::Map<&'static str, AirlineDefinition<'static>> = phf_map! {
    "VS" => AirlineDefinition{
        iata_code: "VS",
        name: "Virgin Atlantic",
        boarding_policy: "GROUP_BASED",
        seat_class_policy: "CABIN_BASED",

        boarding_pass_background_colour: "#4f145b",

        frequent_flyer_program_name: Some("Flying Club"),

        logo_url: "https://p.lukegb.com/raw/VirtuallyCrispViper.png",
        alliance_logo_url: None,
        hero_image_logo_url: Some("https://p.lukegb.com/raw/FormerlyDistinctToad.png"),
        boarding_privilege_logo_url: Some("https://p.lukegb.com/raw/DefinitelyVerifiedTitmouse.png"),
    },
};

fn scan(image_path: &PathBuf) -> Result<String> {
    let image_path_str = (*image_path).to_str().ok_or(anyhow!("invalid path"))?;
    rxing::helpers::detect_in_file(image_path_str, Some(rxing::BarcodeFormat::AZTEC))
        .map_err(|e| {
            anyhow!(
                "could not parse Aztec barcode from image {}: {}",
                image_path_str,
                e
            )
        })
        .map(|r| r.getText().clone())
}

struct ServiceAccount {
    service_account_name: String,
    authenticator: Authenticator<<DefaultHyperClient as HyperClientBuilder>::Connector>,
    key_pair: RS256KeyPair,
}

#[derive(Deserialize)]
struct ServiceAccountJSON {
    // We omit a lot of fields we don't care about.
    /// Service Account email
    client_email: String,

    /// RSA private key
    private_key: String,
}

async fn load_service_account(path: &PathBuf) -> Result<ServiceAccount> {
    let creds = read_service_account_key(path).await?;
    let sa = ServiceAccountAuthenticator::builder(creds).build().await?;

    let sa_data = tokio::fs::read(path).await?;
    let sa_parsed: ServiceAccountJSON = serde_json::from_slice(&sa_data[..])?;
    let key_pair = RS256KeyPair::from_pem(&sa_parsed.private_key)?;

    Ok(ServiceAccount {
        service_account_name: sa_parsed.client_email,
        authenticator: sa,
        key_pair: key_pair,
    })
}

async fn load_thing<'a, T: Deserialize<'a>>(path: &PathBuf) -> Result<T> {
    let data = tokio::fs::read(path).await?;
    let mut deser = serde_json::Deserializer::from_reader(data.as_slice());
    let parsed: T = T::deserialize(&mut deser)?;
    return Ok(parsed);
}

#[async_trait]
trait InsertOrUpdate<T: Clone> {
    async fn insert_now(&self, value: &T) -> Result<bool>;
    async fn update_now(&self, value: &T) -> Result<()>;
}

fn is_already_exists(err: &google_walletobjects1::Error) -> bool {
    match err {
        google_walletobjects1::Error::BadRequest(v) => match v.pointer("/error/code") {
            Some(num) => num == 409,
            None => false,
        },
        _ => false,
    }
}

#[async_trait]
impl<S> InsertOrUpdate<FlightClass> for FlightclasMethods<'_, S>
where
    S: tower_service::Service<http::Uri> + Clone + Send + Sync + 'static,
    S::Response: hyper::client::connect::Connection
        + tokio::io::AsyncRead
        + tokio::io::AsyncWrite
        + Send
        + Unpin
        + 'static,
    S::Future: Send + Unpin + 'static,
    S::Error: Into<Box<dyn std::error::Error + Send + Sync>>,
{
    async fn insert_now(&self, value: &FlightClass) -> Result<bool> {
        self.insert(value.clone())
            .doit()
            .await
            .and(Ok(true))
            .or_else(|e| {
                if is_already_exists(&e) {
                    Ok(false)
                } else {
                    Err(anyhow!(e))
                }
            })
    }
    async fn update_now(&self, value: &FlightClass) -> Result<()> {
        self.update(
            value.clone(),
            &value.id.clone().ok_or(anyhow!("no id on FlightClass"))?,
        )
        .doit()
        .await?;
        Ok(())
    }
}

#[async_trait]
impl<S> InsertOrUpdate<FlightObject> for FlightobjectMethods<'_, S>
where
    S: tower_service::Service<http::Uri> + Clone + Send + Sync + 'static,
    S::Response: hyper::client::connect::Connection
        + tokio::io::AsyncRead
        + tokio::io::AsyncWrite
        + Send
        + Unpin
        + 'static,
    S::Future: Send + Unpin + 'static,
    S::Error: Into<Box<dyn std::error::Error + Send + Sync>>,
{
    async fn insert_now(&self, value: &FlightObject) -> Result<bool> {
        self.insert(value.clone())
            .doit()
            .await
            .and(Ok(true))
            .or_else(|e| {
                if is_already_exists(&e) {
                    Ok(false)
                } else {
                    Err(anyhow!(e))
                }
            })
    }
    async fn update_now(&self, value: &FlightObject) -> Result<()> {
        self.update(
            value.clone(),
            &value.id.clone().ok_or(anyhow!("no id on FlightObject"))?,
        )
        .doit()
        .await?;
        Ok(())
    }
}

#[derive(Serialize, Deserialize)]
struct AddToWalletPayloadObject {
    id: String,

    #[serde(rename = "classId", skip_serializing_if = "Option::is_none")]
    class_id: Option<String>,
}

#[derive(Serialize, Deserialize)]
struct AddToWalletPayload {
    #[serde(skip_serializing_if = "Vec::is_empty", rename = "flightClasses")]
    flight_classes: Vec<AddToWalletPayloadObject>,

    #[serde(skip_serializing_if = "Vec::is_empty", rename = "flightObjects")]
    flight_objects: Vec<AddToWalletPayloadObject>,
}

#[derive(Serialize, Deserialize)]
struct AddToWalletClaims {
    origins: Vec<String>,

    #[serde(rename = "typ")]
    jwt_type: String,

    payload: AddToWalletPayload,
}

#[derive(Clone, Copy, Debug, EnumIter)]
#[repr(u8)]
enum PassengerStatus {
    NotCheckedIn = b'0',
    CheckedIn = b'1',
    BagsCheckedAndNotCheckedIn = b'2',
    BagsCheckedAndCheckedIn = b'3',
    PastSecurity = b'4',
    PastGate = b'5',
    Transit = b'6',
    Standby = b'7',
    BoardingDataRevalidated = b'8',
    OriginalBoardingLineUsedWhenTicketIssued = b'9',
    UpOrDownGradingPending = b'A',
}

impl PassengerStatus {
    fn from(c: u8) -> Result<PassengerStatus> {
        for v in PassengerStatus::iter() {
            if v as u8 == c {
                return Ok(v);
            }
        }
        Err(anyhow!(
            "{} ({}) is not a valid PassengerStatus value",
            c,
            c as char
        ))
    }
}

#[derive(Clone, Copy, Debug, EnumIter)]
#[repr(u8)]
enum DocumentationVerificationStatus {
    NotRequired = b'0',
    Required = b'1',
    Performed = b'2',
}

impl DocumentationVerificationStatus {
    fn from(c: u8) -> Result<DocumentationVerificationStatus> {
        for v in DocumentationVerificationStatus::iter() {
            if v as u8 == c {
                return Ok(v);
            }
        }
        Err(anyhow!(
            "{} ({}) is not a valid DocumentationVerificationStatus value",
            c,
            c as char
        ))
    }
}

#[derive(Clone, Copy, Debug, EnumIter)]
#[repr(u8)]
enum PassengerDescription {
    Adult = b'0',
    Male = b'1',
    Female = b'2',
    Child = b'3',
    Infant = b'4',
    NoPassenger = b'5', // cabin baggage
    AdultWithInfant = b'6',
    UnaccompaniedMinor = b'7',
}

impl PassengerDescription {
    fn from(c: u8) -> Result<PassengerDescription> {
        for v in PassengerDescription::iter() {
            if v as u8 == c {
                return Ok(v);
            }
        }
        Err(anyhow!(
            "{} ({}) is not a valid PassengerDescription value",
            c,
            c as char
        ))
    }
}

#[derive(Clone, Copy, Debug, EnumIter)]
#[repr(u8)]
enum CheckInSource {
    Web = b'W',
    AirportKiosk = b'K',
    RemoteKiosk = b'R',
    MobileDevice = b'M',
    AirportAgent = b'O',
    TownAgent = b'T',
    ThirdPartyVendor = b'V',
}

impl CheckInSource {
    fn from(c: u8) -> Result<CheckInSource> {
        for v in CheckInSource::iter() {
            if v as u8 == c {
                return Ok(v);
            }
        }
        Err(anyhow!(
            "{} ({}) is not a valid CheckInSource value",
            c,
            c as char
        ))
    }
}

#[derive(Debug)]
struct BoardingPass {
    passenger_name: String,
    eticket_indicator: char,

    version_number: Option<char>,
    passenger_description: Option<PassengerDescription>,
    checkin_source: Option<CheckInSource>,
    boarding_pass_source: Option<CheckInSource>,
    date_of_issue: Option<NaiveDate>,
    document_type: Option<char>,
    boarding_pass_issuing_airline: Option<String>,
    baggage_tag_plate_number: Option<String>,

    legs: Vec<BoardingPassLeg>,

    security_data_type: u8,
    security_data: Vec<u8>,
}

fn parse_boarding_pass_date(
    from_boarding_pass_str: &str,
    nearby_date: &NaiveDate,
) -> Result<NaiveDate> {
    // Range is 0 to 9999 (although not really).
    let from_boarding_pass: u16 = from_boarding_pass_str.parse()?;

    match from_boarding_pass_str.len() {
        4 => Ok(NaiveDate::from_yo_opt(
            (nearby_date.year() / 10 * 10) + ((from_boarding_pass / 1000) as i32),
            (from_boarding_pass % 1000).into(),
        )
        .ok_or(anyhow!("could not parse date {}", from_boarding_pass_str))?),
        3 => {
            // Find the one that's the fewest days away.
            let year = nearby_date.year();
            let day: u32 = (from_boarding_pass % 1000).into();
            let mut options = vec![
                NaiveDate::from_yo_opt(year, day),
                NaiveDate::from_yo_opt(year - 1, day),
                NaiveDate::from_yo_opt(year + 1, day),
            ];
            options.sort_by(|a, b| {
                days_between(nearby_date, a)
                    .unwrap_or(9999)
                    .cmp(&days_between(nearby_date, b).unwrap_or(9999))
            });
            Ok(options[0].unwrap())
        }
        _ => Err(anyhow!(
            "boarding pass date {} is wrong length",
            from_boarding_pass_str
        )),
    }
}

fn days_between(nearby_date: &NaiveDate, maybe_date: &Option<NaiveDate>) -> Option<u64> {
    if maybe_date.is_none() {
        return None;
    }
    let is_date = maybe_date.as_ref().unwrap();
    let days_between = (*nearby_date - *is_date).num_days();
    Some(if days_between < 0 {
        (-days_between) as u64
    } else {
        days_between as u64
    })
}

impl BoardingPass {
    fn parse(data: Vec<u8>, nearby_date: &NaiveDate) -> Result<BoardingPass> {
        if data[0] != b'M' {
            return Err(anyhow!(
                "format code was {} ({}), not M",
                data[0] as char,
                data[0]
            ));
        }
        let leg_count = match data[1] {
            b'1' => Ok(1),
            b'2' => Ok(2),
            b'3' => Ok(3),
            b'4' => Ok(4),
            _ => Err(anyhow!(
                "leg count was {} ({}), should be 1 to 4",
                data[1] as char,
                data[1]
            )),
        }?;

        let passenger_name: String = std::str::from_utf8(&data[2..22])?.trim_end().to_string();
        let eticket_indicator: char = data[22] as char;

        // First leg mandatory data:
        let (first_leg, mut p) = BoardingPassLeg::parse_mandatory(&data[23..], nearby_date)?;
        p += 23;

        // We now have enough to start composing the boardingpass.
        let mut boarding_pass = BoardingPass {
            passenger_name,
            eticket_indicator,

            version_number: None,
            passenger_description: None,
            checkin_source: None,
            boarding_pass_source: None,
            date_of_issue: None,
            document_type: None,
            boarding_pass_issuing_airline: None,
            baggage_tag_plate_number: None,

            legs: vec![first_leg],

            security_data_type: 0xff,
            security_data: vec![],
        };

        // Header + first leg optional data (why, IATA, why)
        let optional_data_size: usize =
            usize::from_str_radix(std::str::from_utf8(&data[p..p + 2])?, 16)?;
        let mut optional_data = &data[p + 2..p + 2 + optional_data_size];
        p += 2 + optional_data_size;
        if optional_data.len() >= 1 {
            if optional_data[0] != b'>' {
                return Err(anyhow!("expected Beginning of version number ('>') at beginning of conditional fields, got {} ({})", optional_data[0], optional_data[0] as char));
            }
            optional_data = &optional_data[1..];
        }
        if optional_data.len() >= 1 {
            boarding_pass.version_number = Some(optional_data[0] as char);
            optional_data = &optional_data[1..];
        }
        if optional_data.len() >= 1 {
            let optional_data_unique_size: usize =
                usize::from_str_radix(std::str::from_utf8(&optional_data[0..2])?, 16)?;
            let mut optional_data_unique = &optional_data[2..optional_data_unique_size + 2];
            if optional_data_unique.len() >= 1 {
                boarding_pass.passenger_description = if optional_data_unique[0] == b' ' {
                    None
                } else {
                    Some(PassengerDescription::from(optional_data_unique[0])?)
                };
                optional_data_unique = &optional_data_unique[1..];
            }
            if optional_data_unique.len() >= 1 {
                boarding_pass.checkin_source = if optional_data_unique[0] == b' ' {
                    None
                } else {
                    Some(CheckInSource::from(optional_data_unique[0])?)
                };
                optional_data_unique = &optional_data_unique[1..];
            }
            if optional_data_unique.len() >= 1 {
                boarding_pass.boarding_pass_source = if optional_data_unique[0] == b' ' {
                    None
                } else {
                    Some(CheckInSource::from(optional_data_unique[0])?)
                };
                optional_data_unique = &optional_data_unique[1..];
            }
            if optional_data_unique.len() >= 4 {
                let date_of_issue_str = std::str::from_utf8(&optional_data_unique[0..4])?;
                boarding_pass.date_of_issue = if date_of_issue_str == "    " {
                    None
                } else {
                    Some(parse_boarding_pass_date(date_of_issue_str, nearby_date)?)
                };
                optional_data_unique = &optional_data_unique[4..];
            }
            // TODO: the above might fall through if we don't have 4 bytes of data
            if optional_data_unique.len() >= 1 {
                boarding_pass.document_type = if optional_data_unique[0] == b' ' {
                    None
                } else {
                    Some(optional_data_unique[0] as char)
                };
                optional_data_unique = &optional_data_unique[1..];
            }
            if optional_data_unique.len() >= 3 {
                let issuing_airline_str =
                    std::str::from_utf8(&optional_data_unique[0..3])?.trim_end();
                boarding_pass.boarding_pass_issuing_airline = if issuing_airline_str == "" {
                    None
                } else {
                    Some(issuing_airline_str.to_string())
                };
                optional_data_unique = &optional_data_unique[3..];
            }
            // TODO: the above might fall through if we don't have 3 bytes of data
            let bag_tags_str_len = min(13, optional_data_unique.len());
            let bag_tags_str =
                std::str::from_utf8(&optional_data_unique[0..bag_tags_str_len])?.trim_end();
            boarding_pass.baggage_tag_plate_number = if bag_tags_str == "" {
                None
            } else {
                Some(bag_tags_str.to_string())
            };
            optional_data_unique = &optional_data_unique[bag_tags_str_len..];
            if optional_data_unique.len() != 0 {
                return Err(anyhow!(
                    "trailing unique optional data ({} bytes)",
                    optional_data_unique.len()
                ));
            }
            optional_data = &optional_data[optional_data_unique_size + 2..];
        }

        // Now for the repeated optional data for the first leg.
        boarding_pass.legs[0].add_optional(optional_data)?;

        // Now any additional legs(!)
        for _legnum in 1..leg_count {
            let (mut leg, leg_bytes) = BoardingPassLeg::parse_mandatory(&data[p..], nearby_date)?;
            p += leg_bytes;

            let optional_data_size: usize =
                usize::from_str_radix(std::str::from_utf8(&data[p..p + 2])?, 16)?;
            let optional_data = &data[p + 2..p + 2 + optional_data_size];
            p += 2 + optional_data_size;

            leg.add_optional(optional_data)?;
            boarding_pass.legs.push(leg);
        }

        // What's left is the security data.
        if data[p] != b'^' {
            return Err(anyhow!(
                "expected security data beginning indicator (^) but got {} ({})",
                data[p],
                data[p] as char
            ));
        }
        boarding_pass.security_data_type = data[p + 1];
        let security_data_size: usize =
            usize::from_str_radix(std::str::from_utf8(&data[p + 2..p + 4])?, 16)?;
        boarding_pass.security_data = data[p + 4..p + 4 + security_data_size].to_vec();
        if data.len() > p + 4 + security_data_size {
            return Err(anyhow!(
                "trailing bytes after security data (expected {} bytes of security data)",
                security_data_size
            ));
        }

        Ok(boarding_pass)
    }
}

#[derive(Debug)]
struct BoardingPassLeg {
    operating_carrier_pnr: String, // 7, left justified w/ trailing blanks
    origin_airport: String,        // 3
    destination_airport: String,   // 3
    operating_carrier: String,     // 3, left justified w/ trailing blanks
    flight_number: String,         // 5, NNNN[a] (leading zeros + alpha/blank on last digit)
    date_of_flight: NaiveDate, // 3, leading zeros (this is the day in the year e.g. 001 = Jan 1st,
    // 365 == Dec 31st)
    compartment_code: char,
    seat_number: String,       // 4, NNNa (leading zeros)
    check_in_sequence: String, // NNNN[f] (leading zeros + alpha/blank on last digit)
    passenger_status: PassengerStatus,

    airline_numeric_code: Option<String>,
    document_serial_number: Option<String>,
    selectee_indicator: Option<char>,
    documentation_verification: Option<DocumentationVerificationStatus>,
    marketing_carrier: Option<String>,
    frequent_flyer_airline: Option<String>,
    frequent_flyer_number: Option<String>,
    id_ad_indicator: Option<char>,
    free_baggage_allowance: Option<String>,
    airline_data: Option<Vec<u8>>,
}

impl BoardingPassLeg {
    fn parse_mandatory(data: &[u8], nearby_date: &NaiveDate) -> Result<(BoardingPassLeg, usize)> {
        let operating_carrier_pnr: String =
            std::str::from_utf8(&data[0..7])?.trim_end().to_string();
        let origin_airport: String = std::str::from_utf8(&data[7..10])?.to_string();
        let destination_airport: String = std::str::from_utf8(&data[10..13])?.to_string();
        let operating_carrier: String = std::str::from_utf8(&data[13..16])?.trim_end().to_string();
        let flight_number: String = std::str::from_utf8(&data[16..21])?
            .trim_end()
            .trim_start_matches('0')
            .to_string();
        let date_of_flight_str: &str = std::str::from_utf8(&data[21..24])?;
        let date_of_flight: NaiveDate = parse_boarding_pass_date(date_of_flight_str, nearby_date)?;
        let compartment_code: char = data[24] as char;
        let seat_number: String = std::str::from_utf8(&data[25..29])?
            .trim_start_matches('0')
            .to_string();
        let check_in_sequence: String = std::str::from_utf8(&data[29..34])?
            .trim_end()
            .trim_start_matches('0')
            .to_string();
        let passenger_status: PassengerStatus = PassengerStatus::from(data[34])?;
        Ok((
            BoardingPassLeg {
                operating_carrier_pnr,
                origin_airport,
                destination_airport,
                operating_carrier,
                flight_number,
                date_of_flight,
                compartment_code,
                seat_number,
                check_in_sequence,
                passenger_status,

                airline_numeric_code: None,
                document_serial_number: None,
                selectee_indicator: None,
                documentation_verification: None,
                marketing_carrier: None,
                frequent_flyer_airline: None,
                frequent_flyer_number: None,
                id_ad_indicator: None,
                free_baggage_allowance: None,
                airline_data: None,
            },
            35,
        ))
    }

    fn add_optional(&mut self, data: &[u8]) -> Result<()> {
        let structured_data_size: usize =
            usize::from_str_radix(std::str::from_utf8(&data[0..2])?, 16)?;
        let mut structured_data = &data[2..structured_data_size + 2];

        if structured_data.len() >= 3 {
            let airline_code_str = std::str::from_utf8(&structured_data[0..3])?.trim_end();
            self.airline_numeric_code = if airline_code_str == "" {
                None
            } else {
                Some(airline_code_str.to_string())
            };
            structured_data = &structured_data[3..];
        }
        if structured_data.len() >= 10 {
            let document_serial_number_str =
                std::str::from_utf8(&structured_data[0..10])?.trim_end();
            self.document_serial_number = if document_serial_number_str == "" {
                None
            } else {
                Some(document_serial_number_str.to_string())
            };
            structured_data = &structured_data[10..];
        }
        if structured_data.len() >= 1 {
            self.selectee_indicator = if structured_data[0] == b' ' {
                None
            } else {
                Some(structured_data[0] as char)
            };
            structured_data = &structured_data[1..];
        }
        if structured_data.len() >= 1 {
            self.documentation_verification = if structured_data[0] == b' ' {
                None
            } else {
                Some(DocumentationVerificationStatus::from(structured_data[0])?)
            };
            structured_data = &structured_data[1..];
        }
        if structured_data.len() >= 3 {
            let marketing_carrier_str = std::str::from_utf8(&structured_data[0..3])?.trim_end();
            self.marketing_carrier = if marketing_carrier_str == "" {
                None
            } else {
                Some(marketing_carrier_str.to_string())
            };
            structured_data = &structured_data[3..];
        }
        if structured_data.len() >= 3 {
            let frequent_flyer_airline_str =
                std::str::from_utf8(&structured_data[0..3])?.trim_end();
            self.frequent_flyer_airline = if frequent_flyer_airline_str == "" {
                None
            } else {
                Some(frequent_flyer_airline_str.to_string())
            };
            structured_data = &structured_data[3..];
        }
        if structured_data.len() >= 16 {
            let frequent_flyer_number_str =
                std::str::from_utf8(&structured_data[0..16])?.trim_end();
            self.frequent_flyer_number = if frequent_flyer_number_str == "" {
                None
            } else {
                Some(frequent_flyer_number_str.to_string())
            };
            structured_data = &structured_data[16..];
        }
        if structured_data.len() >= 1 {
            self.id_ad_indicator = if structured_data[0] == b' ' {
                None
            } else {
                Some(structured_data[0] as char)
            };
            structured_data = &structured_data[1..];
        }
        if structured_data.len() >= 3 {
            let free_baggage_allowance_str =
                std::str::from_utf8(&structured_data[0..3])?.trim_end();
            self.free_baggage_allowance = if free_baggage_allowance_str == "" {
                None
            } else {
                Some(free_baggage_allowance_str.to_string())
            };
            structured_data = &structured_data[3..];
        }
        if structured_data.len() != 0 {
            return Err(anyhow!(
                "trailing data in structured data section ({} bytes)",
                structured_data.len()
            ));
        }

        let unstructured_data = &data[structured_data_size + 2..];
        if unstructured_data.len() > 0 {
            self.airline_data = Some(unstructured_data.to_vec());
        }
        Ok(())
    }
}

#[tokio::main]
async fn main() -> Result<()> {
    let cli = Cli::parse();

    let command = cli.command.unwrap();

    match &command {
        Commands::Scan { image } => {
            println!("{}", scan(&image)?);
            return Ok(());
        }
        _ => {}
    };

    let service_account_file = cli
        .service_account_file
        .ok_or(anyhow!("--service-account-file must be set"))?;
    let sa = load_service_account(&service_account_file).await?;

    let hub = google_walletobjects1::Walletobjects::new(
        hyper::Client::builder().build(
            hyper_rustls::HttpsConnectorBuilder::new()
                .with_native_roots()
                .https_or_http()
                .enable_http1()
                .enable_http2()
                .build(),
        ),
        sa.authenticator,
    );

    match &command {
        Commands::Scan { image: _ } => unreachable!(),
        Commands::GetClass(get) => {
            let class_id: String = if get.class_id.contains('.') {
                get.class_id.clone()
            } else {
                format!("{}.{}", cli.issuer_id, get.class_id)
            };

            let (_, class) = hub.flightclass().get(&class_id).doit().await?;
            println!("{}", serde_json::to_string(&class)?);

            Ok(())
        }
        Commands::GetPass(get) => {
            let pass_id: String = if get.pass_id.contains('.') {
                get.pass_id.clone()
            } else {
                format!("{}.{}", cli.issuer_id, get.pass_id)
            };

            let (_, pass) = hub.flightobject().get(&pass_id).doit().await?;
            println!("{}", serde_json::to_string(&pass)?);

            Ok(())
        }
        Commands::Upload(upload) => {
            let (pass, mut class): (FlightObject, FlightClass) = try_join!(
                load_thing(&upload.pass_json),
                load_thing(&upload.class_json)
            )?;

            // Always reset the class state to "UNDER_REVIEW".
            class.review_status = Some("UNDER_REVIEW".to_string());

            // Try to create the class first...
            let created_class: bool = hub.flightclass().insert_now(&class).await?;
            if !created_class {
                // Already exists.
                hub.flightclass().update_now(&class).await?;
            }

            // Then the pass...
            let created_pass: bool = hub.flightobject().insert_now(&pass).await?;
            if !created_pass {
                // Already exists.
                hub.flightobject().update_now(&pass).await?;
            }

            // Now generate the URL.
            let claims: JWTClaims<AddToWalletClaims> = JWTClaims {
                issued_at: None,
                expires_at: None,
                invalid_before: None,
                audiences: Some(jwt_simple::claims::Audiences::AsString(
                    "google".to_string(),
                )),
                issuer: Some(sa.service_account_name),
                jwt_id: None,
                subject: None,
                nonce: None,
                custom: AddToWalletClaims {
                    jwt_type: "savetowallet".to_string(),
                    origins: vec!["www.lukegb.com".to_string()],
                    payload: AddToWalletPayload {
                        flight_classes: vec![AddToWalletPayloadObject {
                            id: class.id.clone().unwrap(),
                            class_id: None,
                        }],
                        flight_objects: vec![AddToWalletPayloadObject {
                            id: pass.id.unwrap(),
                            class_id: Some(class.id.unwrap()),
                        }],
                    },
                },
            };
            let token = sa.key_pair.sign(claims)?;

            println!("https://pay.google.com/gp/v/save/{}", token);

            Ok(())
        }
        Commands::UploadScan(upload_scan) => {
            let scanned = scan(&upload_scan.image)?;
            let boarding_pass =
                BoardingPass::parse(scanned.as_bytes().to_vec(), &Local::now().date_naive())?;
            if boarding_pass.legs.len() > 1 {
                return Err(anyhow!("multi-leg trips aren't supported yet"));
            }
            let leg = &boarding_pass.legs[0];
            let airline_data = AIRLINE_DATA.get(&leg.operating_carrier).ok_or(anyhow!(
                "no AIRLINE_DATA has been provided for {} yet",
                leg.operating_carrier
            ))?;

            let id_prefix = format!(
                "{}.{}{}-{}-{}{}",
                cli.issuer_id,
                leg.operating_carrier,
                leg.flight_number,
                leg.date_of_flight,
                leg.origin_airport,
                leg.destination_airport
            );
            let class_id = &id_prefix;
            let pass_id = format!("{}-{}", &id_prefix, leg.check_in_sequence);

            let mut class = FlightClass::default();
            class.id = Some(class_id.clone());
            class.issuer_name = Some(airline_data.name.to_string());
            class.hex_background_color =
                Some(airline_data.boarding_pass_background_colour.to_string());
            class.review_status = Some("UNDER_REVIEW".to_string());
            class.boarding_and_seating_policy =
                Some(google_walletobjects1::api::BoardingAndSeatingPolicy {
                    boarding_policy: Some(airline_data.boarding_policy.to_string()),
                    seat_class_policy: Some(airline_data.seat_class_policy.to_string()),
                    kind: None,
                });
            class.origin = Some(airport(&leg.origin_airport));
            class.destination = Some(airport(&leg.destination_airport));
            let mut flight_header = google_walletobjects1::api::FlightHeader::default();
            let mut flight_header_carrier = google_walletobjects1::api::FlightCarrier::default();
            flight_header_carrier.airline_alliance_logo =
                airline_data.alliance_logo_url.map(url_to_image);
            flight_header_carrier.airline_logo = Some(url_to_image(airline_data.logo_url));
            flight_header_carrier.airline_name =
                Some(to_localized_string("en-GB", airline_data.name));
            flight_header_carrier.carrier_iata_code = Some(leg.operating_carrier.to_string());
            flight_header.carrier = Some(flight_header_carrier);
            flight_header.flight_number = Some(leg.flight_number.to_string());
            class.flight_header = Some(flight_header);

            let mut pass = FlightObject::default();
            pass.id = Some(pass_id.to_string());
            pass.class_id = Some(class_id.clone());

            Ok(())
        }
    }
}

fn airport(iata_code: &str) -> google_walletobjects1::api::AirportInfo {
    let mut info = google_walletobjects1::api::AirportInfo::default();
    info.airport_iata_code = Some(iata_code.to_string());
    info
}

fn to_localized_string(locale: &str, s: &str) -> google_walletobjects1::api::LocalizedString {
    google_walletobjects1::api::LocalizedString {
        default_value: Some(google_walletobjects1::api::TranslatedString {
            kind: None,
            language: Some(locale.to_string()),
            value: Some(s.to_string()),
        }),
        kind: None,
        translated_values: None,
    }
}

fn url_to_image(url: &str) -> google_walletobjects1::api::Image {
    google_walletobjects1::api::Image {
        kind: None,
        content_description: None,
        source_uri: Some(google_walletobjects1::api::ImageUri {
            description: None,
            localized_description: None,
            uri: Some(url.to_string()),
        }),
    }
}