<template>
  <div class="container is-max-desktop pb-4">
    <section class="section is-small">
      <div
        class="is-block-mobile
        is-flex-tablet-only
        is-flex-desktop
        is-flex-direction-row
        is-justify-content-space-between">
        <div class="my-auto">
          <div class="is-size-1">{{ fund.fundInfo.asset }}</div>
          <div>
            <a :href="fund.fundInfo.telegramLink" target="_blank" class="has-text-grey-light">
              {{ fund.fundInfo.telegramLink }}
            </a>
          </div>
        </div>
        <div
          class="fund-details is-flex is-flex-direction-row is-justify-content-end my-auto py-6">
          <ul>
            <li v-for="(detail, i) in fundDetails" v-bind:key="i">
              <span class="has-text-weight-bold is-size-6 mr-2">{{ detail.title }}</span>
              <span class="is-size-6">{{ detail.val }}</span>
            </li>
          </ul>
        </div>
      </div>
    </section>
    <section class="section is-small">
      <div class="box">
        <div class="title is-size-4 has-text-grey-dark has-text-centered">
          Tracker
        </div>
        <div class="mb-4">
          <div class="level" v-if="fund.fundInfo.bonuses.length > 0">
            <div class="level-item has-text-centered"
                 v-for="bonus in fund.fundInfo.bonuses.sort((v1, v2) => v1.goal - v2.goal)"
                 v-bind:key="bonus.goal"
            >
              <div>
                <p
                  class="heading"
                  :class="amountHeld >= bonus.goal
                ? 'has-text-success' : 'has-text-grey-dark'"
                >
                  {{ bonus.goal.toLocaleString() }} XLM
                </p>
                <p
                  class="title"
                  :class="amountHeld >= bonus.goal
                ? 'has-text-success' : 'has-text-grey-dark'"
                >
                  {{ bonus.percent }}%
                </p>
              </div>
            </div>
          </div>
          <div v-else>
            <div class="has-text-centered">This group fund has no rewards available</div>
          </div>
        </div>
        <progress class="progress is-large is-info" :value="calcPctComplete()"
                  max="100">
          {{ calcPctComplete() }}%
        </progress>
      </div>
    </section>
    <section class="section is-small">
      <div class="title is-size-4 has-text-white-ter has-text-centered">
        Contribute
      </div>
      <ErrorDisplay :errors="errs" v-if="invalidContributionForm"/>
      <div class="control my-2">
        <input class="input is-normal has-background-white has-text-black" type="text"
               placeholder="Private Key" aria-label="Wallet" v-model="pk" @blur="queryAccount">
      </div>
      <div class="control my-2" :class="loading.balance ? 'is-loading' : null">
        <input
          class="input is-normal has-background-white has-text-black"
          type="number"
          placeholder="Amount"
          aria-label="Amount"
          v-model="amount"
          :max="acctBalance"
          :disabled="loading.balance"
        >
      </div>
      <div class="is-flex is-justify-content-end">
        <button
          class="button is-primary"
          :class="loading.contribution ? 'is-loading' : ''"
          :disabled="invalidContributionForm"
          @click="makeContribution"
        >Submit
        </button>
      </div>
    </section>
    <section class="section is-small" v-if="contributions.length > 0">
      <div class="title is-size-4 has-text-white-ter has-text-centered">
        Contributions
      </div>
      <div class="is-flex is-justify-content-space-between is-rounded my-2">
        <div class="select">
          <select v-model="selectedDate" aria-label="Filter by date">
            <option v-for="date in selectableDates" v-bind:key="date" :value="date">
              {{ date ?? 'Cutoff Date' }}
            </option>
          </select>
        </div>
        <div class="consolidate-option has-background-white px-4 py-1 my-auto">
          <label for="consolidate" class="checkbox has-text-dark is-size-6">
            <span class="consolidate-label is-inline-block">
              Consolidate wallets
            </span>
            <input
              type="checkbox"
              class="ml-2"
              id="consolidate"
              aria-label="Consolidate wallets"
              v-model="enableConsolidation"
            >
          </label>
        </div>
      </div>
      <p class="py-2" v-if="store.getters.token && hasPermission(Privileges.Admin)">
        Enable contribution consolidation in order to select.
        Click to select a row in order to mark a wallet to receive rewards.
      </p>
      <div id="contribution-container" @scroll="loadMoreIfNeeded">
        <table class="contribution-table table is-fullwidth">
          <thead>
          <tr>
            <th>Wallet</th>
            <th>Amount</th>
            <th v-if="!enableConsolidation">Time</th>
            <th v-else>Bonus</th>
          </tr>
          </thead>
          <tbody>
          <tr v-for="(contribution, i) in contributions"
              :class="{ 'contribution-selected': contribution.selected }"
              v-bind:key="i" @click="selectContribution(contribution)">
            <td>{{ truncateWallet(contribution.wallet, calculateWalletChars(), undefined) }}</td>
            <td>{{ contribution.amount }}</td>
            <td v-if="!enableConsolidation">
              <span class="transaction-date" :title="formatDate(contribution.createdAt, true)">
                {{ formatDate(contribution.createdAt, true) }}
              </span>
            </td>
            <td v-else>
              <span>{{ calculateReward(contribution.amount.div(fund.fundInfo.price)) }}</span>
            </td>
          </tr>
          </tbody>
        </table>
      </div>
      <div class="my-4" v-if="store.getters.token && hasPermission(Privileges.Admin)">
        <ButtonWithConfirmation
          :condition="selectedContributions.length === 0"
          :inputs="{ button: { label: 'Distribute Rewards', style: 'is-success' },
            checkbox: { label: 'Allow Distribution' }}"
          :modification="distributeFund"
          @confirmed="setAllowDistribution"
        />
      </div>
    </section>
    <section class="section is-small px-0"
             v-if="store.getters.token && hasPermission(Privileges.Admin)">
      <div class="title is-size-4 has-text-white-ter has-text-centered">
        Submit Group Fund
      </div>
      <ButtonWithConfirmation
        :inputs="{ button: { label: 'Submit Fund', style: 'is-success' },
        checkbox: { label: 'Allow Submission' }}"
        :modification="submitFund"
        @confirmed="setAllowSubmit"
      />
    </section>
    <section v-if="store.getters.token && hasPermission(Privileges.AdminPlus)">
      <div class="title is-size-4 has-text-white-ter has-text-centered">
        Close Group Fund
      </div>
      <ButtonWithConfirmation
        :inputs="{ button: { label: 'Close Fund', style: 'is-danger' },
        checkbox: { label: 'Allow Closing' }}"
        :modification="deleteFund"
        @confirmed="setAllowDelete"
      />
    </section>
  </div>
</template>

<script setup lang="ts">
import {
  useRoute,
  useRouter,
} from 'vue-router';
import {
  Bonus,
  Contribution,
  FundInfo,
  GetRewardFundResponse,
  Privileges,
} from '@/api/types';
import {
  computed,
  Ref,
  ref,
  watch,
} from 'vue';
import {
  useWebSocket,
  useWindowSize,
} from '@vueuse/core';
import store from '@/store';
import {
  sanitize,
  truncateWallet,
} from '@/lib/helpers';
import * as luxon from 'luxon';
import hasPermission from '@/lib/auth';
import ErrorDisplay, { SignetError } from '@/components/ErrorDisplay.vue';
import {
  contribute,
  deleteRewardFund,
  distributeRewardFund,
  getBalance,
  getContributions,
  getRewardFund,
  submitRewardFund,
} from '@/api/composed';
import Decimal from 'decimal.js';
import ButtonWithConfirmation from '@/components/ButtonWithConfirmation.vue';

const route = useRoute();
const router = useRouter();
const { id } = route.params;

const identifier = parseInt(id as string, 10);

const formatDate = (time: string, includeTime = false) => {
  const s = luxon.DateTime.fromISO(time)
    .toUTC();
  const date = s.toFormat('yyyy-LLL-dd');
  return includeTime ? `${date} ${s.toFormat('HH:mm')} (UTC)` : date;
};

const pk = ref('');
const amt = ref(undefined as Decimal | undefined);
const selectableDates = ref([undefined] as (string | undefined)[]);
const selectedDate = ref(undefined as string | undefined);

const { width } = useWindowSize();

const amount = computed({
  get: () => amt.value,
  set: (v) => {
    if (v) {
      amt.value = new Decimal(v);
    } else {
      amt.value = undefined;
    }
  },
});

const enableConsolidation = ref(false);
const fund = ref(
  {
    fundInfo: {} as FundInfo,
    contributions: {
      list: [],
      dates: [] as string[],
      total: 0,
    },
    total: { amountHeld: 0 },
  } as GetRewardFundResponse | null,
);
const fundDetails = ref([{
  title: '',
  val: '',
}]);

fund.value = await getRewardFund(identifier, enableConsolidation.value);
if (!fund.value) {
  router.push('/');
  throw new Error('Fund not found');
}
fundDetails.value = [
  {
    title: 'Asset',
    val: fund.value.fundInfo.asset,
  },
  {
    title: 'Minimum',
    val: `${fund.value.fundInfo.minContribution.toLocaleString()}`,
  },
  {
    title: 'Remaining',
    val: `${fund.value.fundInfo.amountAvailable.toLocaleString()}`,
  },
  {
    title: 'Memo',
    val: `"${fund.value.fundInfo.memo}"`,
  },
];
if (fund.value.contributions.dates) {
  selectableDates.value = selectableDates.value.concat(
    fund.value.contributions.dates.map((d) => formatDate(d)),
  );
}

interface SelectableContribution extends Contribution {
  selected: boolean;
}

const processContributions = (contributions: Contribution[]) => contributions.map((c) => ({
  ...c,
  amount: new Decimal(c.amount),
  selected: false,
}));

const reward = ref(new Decimal(0));
const maxBonus = ref(0);
const bonus = ref(undefined as Bonus | undefined);
const amountHeld = ref(new Decimal(fund.value.total.amountHeld));
const amountAvailable = ref(new Decimal(fund.value.fundInfo.amountAvailable));
const contributions: Ref<SelectableContribution[]> = ref(processContributions(
  fund.value.contributions.list ?? [],
));
const offset = ref(contributions.value.length);
const total = ref(fund.value.contributions.total);
const contributionsLoading = ref(false);
const acctBalance = ref(undefined as Decimal | undefined);
const unknownAcct = ref(true);
const loading = ref({
  contribution: false,
  balance: false,
});

const round = (num: number, figures = 1) => {
  const factor = 10 ** figures;
  return Math.round(num * factor) / factor;
};

const calcPctComplete = () => round(amountHeld.value.div(amountAvailable.value)
  .mul(100)
  .toNumber());

const calculateWalletChars = () => round(width.value / 110, 0);

const hasInvalidValues = () => {
  if (!fund.value) throw new Error('Fund was not loaded!');
  if ([pk, amount].some((v) => v.value === undefined || v.value === '')) return false;
  if (!pk.value || !amount.value) throw new Error('One or more validation values were undefined');
  return (amount.value.isZero()
    || amount.value > amountAvailable.value
    || amount.value.lt(fund.value.fundInfo.minContribution)
    || (acctBalance.value && amount.value.gt(acctBalance.value))
    || unknownAcct.value);
};

const invalidContributionForm = computed(() => hasInvalidValues());

const getCurrentBonus = () => {
  if (!fund.value) throw new Error('Fund not found!');
  const achievedBonuses = fund.value.fundInfo.bonuses.filter(
    (b) => {
      if (!fund.value) throw new Error('Fund not found');
      return b.goal && fund.value.total.amountHeld >= b.goal;
    },
  );
  if (achievedBonuses.length > 0) {
    maxBonus.value = Math.max(...achievedBonuses.map((b) => b.goal ?? -1));
    bonus.value = achievedBonuses.find((b) => b.goal === maxBonus.value);
    if (!Object.entries(bonus).length) throw new Error('Something went wrong');
  }
};

const calculateReward = (bought: Decimal) => {
  if (bonus.value) {
    if (!bonus.value.percent) throw new Error('Bonus did not have percent for some reason');
    reward.value = bought.add(bought)
      .mul(new Decimal(bonus.value.percent / 100));
  } else {
    reward.value = bought;
  }
  return reward.value.toLocaleString();
};

const selectedContributions = ref([] as Contribution[]);
const selectContribution = (contribution: SelectableContribution) => {
  if (!store.getters.token || !hasPermission(Privileges.Admin)) return;
  if (enableConsolidation.value) {
    if (!contribution.selected) {
      selectedContributions.value.push(contribution);
    } else {
      const existingContributionIndex = selectedContributions.value.findIndex(
        (c) => c.wallet === contribution.wallet,
      );
      selectedContributions.value.splice(existingContributionIndex, 1);
    }
    const newContribution: SelectableContribution = {
      ...contribution,
      selected: !contribution.selected,
    };
    const toReplaceIndex = contributions.value.findIndex((c) => c.wallet === contribution.wallet);
    if (toReplaceIndex === -1) throw new Error('Could not find contribution to select');
    contributions.value.splice(toReplaceIndex, 1, newContribution);
  }
};

const allowDelete = ref(false);
const allowDistribution = ref(false);
const allowSubmit = ref(false);
const setAllowDelete = (val: boolean) => {
  allowDelete.value = val;
};
const setAllowDistribution = (val: boolean) => {
  allowDistribution.value = val;
};
const setAllowSubmit = (val: boolean) => {
  allowSubmit.value = val;
};

const deleteFund = async () => {
  const deleted = await deleteRewardFund(identifier, allowDelete.value);
  if (deleted && deleted.success) {
    await router.push('/');
  }
};

const distributeFund = async () => {
  const distributed = await distributeRewardFund(
    identifier,
    selectedContributions.value.map((c) => ({
      destination: c.wallet,
      amount: c.amount.toNumber(),
    })),
    allowDistribution.value,
  );
  if (distributed && distributed.success) {
    console.log('distributed'); // TODO: provide feedback for distribution
  }
};

const submitFund = async () => {
  const submitted = await submitRewardFund(identifier, allowSubmit.value);
  if (submitted && submitted.success) {
    console.log('submitted'); // TODO: provide feedback for submission
  }
};

const errs: SignetError[] = [
  {
    text: 'Amount is required',
    condition: amt.value === undefined,
  },
  {
    text: 'Amount must be greater than 0',
    condition: amt.value && amt.value.isZero() && !fund.value.fundInfo.minContribution,
  },
  {
    text: 'Amount is less than the minimum contribution',
    condition: amt.value && amt.value.lt(fund.value.fundInfo.minContribution),
  },
  {
    text: `Not enough ${fund.value.fundInfo.asset} for sale in ICO`,
    condition: amt.value && amt.value > amountAvailable.value,
  },
  {
    text: `Not enough XLM to send (${amt.value?.toLocaleString()})`,
    condition: amt.value && acctBalance.value && amt.value.gt(acctBalance.value),
  },
  {
    text: 'Could not find Stellar wallet',
    condition: unknownAcct,
  },
];

document.title = `Beignet - ${fund.value.fundInfo.asset}`;

watch(selectedDate, async (newVal) => {
  offset.value = 0;
  const conts = await getContributions(identifier, offset.value, newVal, enableConsolidation.value);
  if (!fund.value) throw new Error('Fund not found');
  if (!conts) throw new Error('Contributions not found');
  contributions.value = processContributions(conts.list);
  offset.value = contributions.value.length;
  total.value = fund.value.contributions.total;
});

const loadMoreIfNeeded = async (e: Event) => {
  const target = e.target as Element | null;
  const canLoadMore = () => target
    && (target.scrollTop + target.clientHeight) / target.scrollHeight > 0.8
    && offset.value < total.value && !contributionsLoading.value;

  if (canLoadMore()) {
    contributionsLoading.value = true;
    const moreContribs = await getContributions(
      identifier,
      offset.value,
      selectedDate.value,
      enableConsolidation.value,
    );
    if (!moreContribs) throw new Error('Contributions not found');
    offset.value += moreContribs.list.length;
    total.value = moreContribs.total;
    contributions.value = contributions.value.concat(processContributions(moreContribs.list));
    contributionsLoading.value = false;
  }
};

const {
  status,
  data,
} = useWebSocket(
  'ws://127.0.0.1:7300/ContributorStream',
  {
    immediate: true,
    autoReconnect: true,
  },
);

const queryAccount = async () => {
  if (pk.value && pk.value.startsWith('S')) {
    loading.value.balance = true;
    const resp = await getBalance(pk.value);
    if (resp === null) {
      unknownAcct.value = true;
      acctBalance.value = undefined;
    } else {
      unknownAcct.value = false;
      if (resp && resp.balance) {
        acctBalance.value = new Decimal(resp.balance);
        if (amt.value && amt.value.gt(acctBalance.value)) {
          amt.value = acctBalance.value;
        }
      }
    }
    loading.value.balance = false;
  }
};

const makeContribution = async () => {
  if (!fund.value) throw new Error('Fund not found');
  if (!amount.value) return;
  if (!/^[0-9]+$/.test(amount.value.toString())) return;
  if (unknownAcct.value) return;
  if (!loading.value.contribution && pk.value
    && amount.value && amount.value <= amountAvailable.value) {
    loading.value.contribution = true;
    await contribute(sanitize(pk.value), amount.value.toNumber(), fund.value.fundInfo.id);
    loading.value.contribution = false;
    pk.value = '';
    amt.value = undefined;
  }
};

watch(enableConsolidation, async () => {
  offset.value = 0;
  const conts = await getContributions(
    identifier,
    offset.value,
    selectedDate.value,
    enableConsolidation.value,
  );
  if (!fund.value) throw new Error('Fund not found');
  if (!conts) throw new Error('Contributions not found');
  contributions.value = processContributions(conts.list);
  offset.value = contributions.value.length;
  total.value = fund.value.contributions.total;
});

watch(data, (newVal) => {
  if (!fund.value) throw new Error('Fund not found');
  getCurrentBonus();
  if (status.value === 'OPEN') {
    const v = {
      ...JSON.parse(newVal.trim()),
      selected: false,
    } as SelectableContribution;
    v.createdAt = luxon.DateTime.now()
      .toISO();
    const formattedDate = formatDate(v.createdAt);
    if (!selectableDates.value.includes(formattedDate)) {
      selectableDates.value.push(formattedDate);
    }
    if (enableConsolidation.value && contributions.value
      && contributions.value.map((c: Contribution) => c.wallet)
        .includes(v.wallet)) {
      const hasContribution = contributions.value.find((c: Contribution) => c.wallet === v.wallet);
      if (!hasContribution) throw new Error('Something went wrong');
      hasContribution.amount = new Decimal(hasContribution.amount).add(v.amount);
    } else {
      contributions.value.splice(0, 0, v);
      offset.value += 1;
    }
    amountHeld.value = new Decimal(amountHeld.value).add(v.amount);
    amountAvailable.value = new Decimal(amountAvailable.value).sub(v.amount);
  }
});
</script>

<style scoped lang="stylus">
.transaction-date
  white-space nowrap
  overflow hidden
  max-width 20vw
  display block

.consolidate-label
  max-width: 24vw;
  white-space: nowrap;
  overflow: hidden;
  vertical-align: bottom;
  text-overflow: ellipsis;

.total-label, .remaining-label
  border-right 1px solid #8f8f8f
  font-variant all-petite-caps

.signet-asset-name
  cursor pointer

tbody tr
  cursor pointer

#contribution-container
  max-height 360px
  overflow-y auto

.consolidate-option
  border-radius 16px

.fund-description
  min-height 280px

.fund-details
  width 182px

.contribution-selected
  background-color #e0cdbb

#consolidate
  vertical-align middle

@media screen and (min-width: 1024px)
  .fund-details
    max-width 14vw
    border-left 1px #777 solid
    padding-left 10px
</style>
