/**
 * This module defines custom PropTypes shared within the application.
 *
 * To learn about validating React component props with PropTypes, see:
 *
 *     https://facebook.github.io/react/docs/typechecking-with-proptypes.html
 *
 * For component specific PropTypes, it's perfectly ok to inline them
 * to the component itself. If the type is shared or external (SDK or
 * API), however, it should be in this file for sharing with other
 * components.
 *
 * PropTypes should usually be validated only at the lowest level
 * where the props are used, not along the way in parents that pass
 * along the props to their children. Those parents should usually
 * just validate the presense of the prop key and that the value is
 * defined. This way we get the validation errors only in the most
 * specific place and avoid duplicate errros.
 */
import {
  arrayOf,
  bool,
  func,
  instanceOf,
  number,
  object,
  objectOf,
  oneOf,
  oneOfType,
  shape,
  string,
} from 'prop-types';
import Decimal from 'decimal.js';
import { types as sdkTypes } from './sdkLoader';
import { TRANSITIONS, TX_TRANSITION_ACTORS } from './transaction';

const { UUID, LatLng, LatLngBounds, Money } = sdkTypes;

const propTypes = {};

// Fixed value
propTypes.value = val => oneOf([val]);

// SDK type instances
propTypes.uuid = instanceOf(UUID);
propTypes.latlng = instanceOf(LatLng);
propTypes.latlngBounds = instanceOf(LatLngBounds);
propTypes.money = instanceOf(Money);

// Configuration for currency formatting
propTypes.currencyConfig = shape({
  style: string.isRequired,
  currency: string.isRequired,
  currencyDisplay: string,
  useGrouping: bool,
  minimumFractionDigits: number,
  maximumFractionDigits: number,
});

// Configuration for a single route
propTypes.route = shape({
  name: string.isRequired,
  path: string.isRequired,
  exact: bool,
  strict: bool,
  component: func.isRequired,
  loadData: func,
});

// Place object from LocationAutocompleteInput
propTypes.place = shape({
  address: string.isRequired,
  origin: propTypes.latlng,
  bounds: propTypes.latlngBounds, // optional viewport bounds
});

// Denormalised image object
propTypes.image = shape({
  id: propTypes.uuid.isRequired,
  type: propTypes.value('image').isRequired,
  attributes: shape({
    variants: objectOf(
      shape({
        width: number.isRequired,
        height: number.isRequired,
        url: string.isRequired,
      })
    ),
  }),
});

// Denormalised user object
propTypes.currentUser = shape({
  id: propTypes.uuid.isRequired,
  type: propTypes.value('currentUser').isRequired,
  attributes: shape({
    banned: bool.isRequired,
    email: string.isRequired,
    emailVerified: bool.isRequired,
    profile: shape({
      firstName: string.isRequired,
      lastName: string.isRequired,
      displayName: string.isRequired,
      abbreviatedName: string.isRequired,
      bio: string,
    }).isRequired,
    stripeConnected: bool,
  }),
  profileImage: propTypes.image,
});

const userAttributes = shape({
  banned: propTypes.value(false).isRequired,
  deleted: propTypes.value(false).isRequired,
  profile: shape({
    displayName: string.isRequired,
    abbreviatedName: string.isRequired,
    bio: string,
  }),
});

// Listing queries can include author.
// Banned and deleted are not relevant then
// since banned and deleted users can't have listings.
const authorAttributes = shape({
  profile: shape({
    displayName: string.isRequired,
    abbreviatedName: string.isRequired,
    bio: string,
  }),
});

const deletedUserAttributes = shape({
  deleted: propTypes.value(true).isRequired,
});

const bannedUserAttributes = shape({
  banned: propTypes.value(true).isRequired,
});

// Denormalised user object
propTypes.user = shape({
  id: propTypes.uuid.isRequired,
  type: propTypes.value('user').isRequired,
  attributes: oneOfType([
    userAttributes,
    authorAttributes,
    deletedUserAttributes,
    bannedUserAttributes,
  ]).isRequired,
  profileImage: propTypes.image,
});

export const LISTING_STATE_DRAFT = 'draft';
export const LISTING_STATE_PENDING_APPROVAL = 'pendingApproval';
export const LISTING_STATE_PUBLISHED = 'published';
export const LISTING_STATE_CLOSED = 'closed';

const LISTING_STATES = [
  LISTING_STATE_DRAFT,
  LISTING_STATE_PENDING_APPROVAL,
  LISTING_STATE_PUBLISHED,
  LISTING_STATE_CLOSED,
];

const listingAttributes = shape({
  title: string.isRequired,
  description: string,
  geolocation: propTypes.latlng,
  deleted: propTypes.value(false),
  state: oneOf(LISTING_STATES),
  price: propTypes.money,
  publicData: object,
});

const AVAILABILITY_PLAN_DAY = 'availability-plan/day';
const AVAILABILITY_PLAN_TIME = 'availability-plan/time';
export const DAYS_OF_WEEK = ['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun'];

const availabilityPlan = shape({
  type: oneOf([AVAILABILITY_PLAN_DAY, AVAILABILITY_PLAN_TIME]).isRequired,
  timezone: string,
  entries: arrayOf(
    shape({
      dayOfWeek: oneOf(DAYS_OF_WEEK).isRequired,
      seats: number.isRequired,
      start: string,
      end: string,
    })
  ),
});

const ownListingAttributes = shape({
  title: string.isRequired,
  description: string,
  geolocation: propTypes.latlng,
  deleted: propTypes.value(false).isRequired,
  state: oneOf(LISTING_STATES).isRequired,
  price: propTypes.money,
  availabilityPlan: availabilityPlan,
  publicData: object.isRequired,
});

const deletedListingAttributes = shape({
  deleted: propTypes.value(true).isRequired,
});

// Denormalised listing object
propTypes.listing = shape({
  id: propTypes.uuid.isRequired,
  type: propTypes.value('listing').isRequired,
  attributes: oneOfType([listingAttributes, deletedListingAttributes]).isRequired,
  author: propTypes.user,
  images: arrayOf(propTypes.image),
});

// Denormalised ownListing object
propTypes.ownListing = shape({
  id: propTypes.uuid.isRequired,
  type: propTypes.value('ownListing').isRequired,
  attributes: oneOfType([ownListingAttributes, deletedListingAttributes]).isRequired,
  author: propTypes.currentUser,
  images: arrayOf(propTypes.image),
});

export const BOOKING_STATE_PENDING = 'pending';
export const BOOKING_STATE_ACCEPTED = 'accepted';
export const BOOKING_STATE_DECLINED = 'declined';
export const BOOKING_STATE_CANCELLED = 'cancelled';
export const BOOKING_STATES = [
  BOOKING_STATE_PENDING,
  BOOKING_STATE_ACCEPTED,
  BOOKING_STATE_DECLINED,
  BOOKING_STATE_CANCELLED,
];

// Denormalised booking object
propTypes.booking = shape({
  id: propTypes.uuid.isRequired,
  type: propTypes.value('booking').isRequired,
  attributes: shape({
    end: instanceOf(Date).isRequired,
    start: instanceOf(Date).isRequired,
    displayStart: instanceOf(Date),
    displayEnd: instanceOf(Date),
    state: oneOf(BOOKING_STATES),
  }),
});

// A time slot that covers one day, having a start and end date.
export const TIME_SLOT_DAY = 'time-slot/day';

// Denormalised time slot object
propTypes.timeSlot = shape({
  id: propTypes.uuid.isRequired,
  type: propTypes.value('timeSlot').isRequired,
  attributes: shape({
    type: oneOf([TIME_SLOT_DAY]).isRequired,
    end: instanceOf(Date).isRequired,
    start: instanceOf(Date).isRequired,
  }),
});

// Denormalised availability exception object
propTypes.availabilityException = shape({
  id: propTypes.uuid.isRequired,
  type: propTypes.value('availabilityException').isRequired,
  attributes: shape({
    end: instanceOf(Date).isRequired,
    seats: number.isRequired,
    start: instanceOf(Date).isRequired,
  }),
});

propTypes.transition = shape({
  createdAt: instanceOf(Date).isRequired,
  by: oneOf(TX_TRANSITION_ACTORS).isRequired,
  transition: oneOf(TRANSITIONS).isRequired,
});

// Possible amount of stars in a review
export const REVIEW_RATINGS = [1, 2, 3, 4, 5];

// Review types: review of a provider and of a customer
export const REVIEW_TYPE_OF_PROVIDER = 'ofProvider';
export const REVIEW_TYPE_OF_CUSTOMER = 'ofCustomer';

// A review on a user
propTypes.review = shape({
  id: propTypes.uuid.isRequired,
  attributes: shape({
    createdAt: instanceOf(Date).isRequired,
    content: string,
    rating: oneOf(REVIEW_RATINGS),
    state: string.isRequired,
    type: oneOf([REVIEW_TYPE_OF_PROVIDER, REVIEW_TYPE_OF_CUSTOMER]).isRequired,
  }).isRequired,
  author: propTypes.user,
  subject: propTypes.user,
});

// A Stripe account entity
propTypes.stripeAccount = shape({
  id: propTypes.uuid.isRequired,
  type: propTypes.value('stripeAccount').isRequired,
  attributes: shape({
    stripeAccountId: string.isRequired,
  }),
});

export const LINE_ITEM_NIGHT = 'line-item/night';
export const LINE_ITEM_DAY = 'line-item/day';
export const LINE_ITEM_UNITS = 'line-item/units';
export const LINE_ITEM_CUSTOMER_COMMISSION = 'line-item/customer-commission';
export const LINE_ITEM_PROVIDER_COMMISSION = 'line-item/provider-commission';

export const LINE_ITEMS = [
  LINE_ITEM_NIGHT,
  LINE_ITEM_DAY,
  LINE_ITEM_UNITS,
  LINE_ITEM_CUSTOMER_COMMISSION,
  LINE_ITEM_PROVIDER_COMMISSION,
];

propTypes.bookingUnitType = oneOf([LINE_ITEM_NIGHT, LINE_ITEM_DAY, LINE_ITEM_UNITS]);

const requiredLineItemPropType = (props, propName, componentName) => {
  const prop = props[propName];

  if (!prop || prop === '') {
    return new Error(`Missing line item code prop from ${componentName}.`);
  }
  if (!/^line-item\/.+/.test(prop)) {
    return new Error(`Invalid line item code value ${prop} passed to ${componentName}.`);
  }
};

// Denormalised transaction object
propTypes.transaction = shape({
  id: propTypes.uuid.isRequired,
  type: propTypes.value('transaction').isRequired,
  attributes: shape({
    createdAt: instanceOf(Date),
    lastTransitionedAt: instanceOf(Date).isRequired,
    lastTransition: oneOf(TRANSITIONS).isRequired,

    // An enquiry won't need a total sum nor a booking so these are
    // optional.
    payinTotal: propTypes.money,
    payoutTotal: propTypes.money,

    lineItems: arrayOf(
      shape({
        code: requiredLineItemPropType,
        includeFor: arrayOf(oneOf(['customer', 'provider'])).isRequired,
        quantity: instanceOf(Decimal),
        unitPrice: propTypes.money.isRequired,
        lineTotal: propTypes.money.isRequired,
        reversal: bool.isRequired,
      })
    ),
    transitions: arrayOf(propTypes.transition).isRequired,
  }),
  booking: propTypes.booking,
  listing: propTypes.listing,
  customer: propTypes.user,
  provider: propTypes.user,
  reviews: arrayOf(propTypes.review),
});

// Denormalised transaction message
propTypes.message = shape({
  id: propTypes.uuid.isRequired,
  type: propTypes.value('message').isRequired,
  attributes: shape({
    createdAt: instanceOf(Date).isRequired,
    content: string.isRequired,
  }).isRequired,
  sender: propTypes.user,
});

// Pagination information in the response meta
propTypes.pagination = shape({
  page: number.isRequired,
  perPage: number.isRequired,
  totalItems: number.isRequired,
  totalPages: number.isRequired,
});

// Search filter definition
const filterWithOptions = shape({
  paramName: string.isRequired,
  options: arrayOf(
    shape({
      key: oneOfType([string, bool, number]).isRequired,
      label: string.isRequired,
    })
  ).isRequired,
});
const filterWithPriceConfig = shape({
  paramName: string.isRequired,
  config: shape({
    min: number.isRequired,
    max: number.isRequired,
    step: number.isRequired,
  }).isRequired,
});

const filterIsActiveConfig = shape({
  paramName: string.isRequired,
  config: shape({
    active: bool.isRequired,
  }).isRequired,
});

propTypes.filterConfig = oneOfType([
  filterWithOptions,
  filterWithPriceConfig,
  filterIsActiveConfig,
]);

export const ERROR_CODE_TRANSACTION_LISTING_NOT_FOUND = 'transaction-listing-not-found';
export const ERROR_CODE_TRANSACTION_INVALID_TRANSITION = 'transaction-invalid-transition';
export const ERROR_CODE_TRANSACTION_ALREADY_REVIEWED_BY_CUSTOMER =
  'transaction-already-reviewed-by-customer';
export const ERROR_CODE_TRANSACTION_ALREADY_REVIEWED_BY_PROVIDER =
  'transaction-already-reviewed-by-provider';
export const ERROR_CODE_TRANSACTION_BOOKING_TIME_NOT_AVAILABLE =
  'transaction-booking-time-not-available';
export const ERROR_CODE_PAYMENT_FAILED = 'transaction-payment-failed';
export const ERROR_CODE_CHARGE_ZERO_PAYIN = 'transaction-charge-zero-payin';
export const ERROR_CODE_CHARGE_ZERO_PAYOUT = 'transaction-charge-zero-payout';
export const ERROR_CODE_EMAIL_TAKEN = 'email-taken';
export const ERROR_CODE_EMAIL_NOT_FOUND = 'email-not-found';
export const ERROR_CODE_TOO_MANY_VERIFICATION_REQUESTS = 'email-too-many-verification-requests';
export const ERROR_CODE_UPLOAD_OVER_LIMIT = 'request-upload-over-limit';
export const ERROR_CODE_VALIDATION_INVALID_PARAMS = 'validation-invalid-params';
export const ERROR_CODE_VALIDATION_INVALID_VALUE = 'validation-invalid-value';
export const ERROR_CODE_NOT_FOUND = 'not-found';
export const ERROR_CODE_FORBIDDEN = 'forbidden';
export const ERROR_CODE_MISSING_STRIPE_ACCOUNT = 'transaction-missing-stripe-account';

const ERROR_CODES = [
  ERROR_CODE_TRANSACTION_LISTING_NOT_FOUND,
  ERROR_CODE_TRANSACTION_INVALID_TRANSITION,
  ERROR_CODE_TRANSACTION_ALREADY_REVIEWED_BY_CUSTOMER,
  ERROR_CODE_TRANSACTION_ALREADY_REVIEWED_BY_PROVIDER,
  ERROR_CODE_PAYMENT_FAILED,
  ERROR_CODE_CHARGE_ZERO_PAYIN,
  ERROR_CODE_CHARGE_ZERO_PAYOUT,
  ERROR_CODE_EMAIL_TAKEN,
  ERROR_CODE_EMAIL_NOT_FOUND,
  ERROR_CODE_TOO_MANY_VERIFICATION_REQUESTS,
  ERROR_CODE_UPLOAD_OVER_LIMIT,
  ERROR_CODE_VALIDATION_INVALID_PARAMS,
  ERROR_CODE_VALIDATION_INVALID_VALUE,
  ERROR_CODE_NOT_FOUND,
  ERROR_CODE_FORBIDDEN,
  ERROR_CODE_MISSING_STRIPE_ACCOUNT,
];

// API error
// TODO this is likely to change soonish
propTypes.apiError = shape({
  id: propTypes.uuid.isRequired,
  status: number.isRequired,
  code: oneOf(ERROR_CODES).isRequired,
  title: string.isRequired,
  meta: object,
});

// Storable error prop type. (Error object should not be stored as it is.)
propTypes.error = shape({
  type: propTypes.value('error').isRequired,
  name: string.isRequired,
  message: string,
  status: number,
  statusText: string,
  apiErrors: arrayOf(propTypes.apiError),
});

export { propTypes };
