<script setup>

import { ref, computed, useTemplateRef, onMounted, inject, watch } from 'vue';
import { marked } from 'marked';
import { Button, Dialog, SingleSelect, FormGroup, TextInput, InputGroup, Spinner } from '@cloudron/pankow';
import { prettyDate, prettyBinarySize } from '@cloudron/pankow/utils';
import AccessControl from './AccessControl.vue';
import PortBindings from './PortBindings.vue';
import AppsModel from '../models/AppsModel.js';
import DomainsModel from '../models/DomainsModel.js';
import UsersModel from '../models/UsersModel.js';
import GroupsModel from '../models/GroupsModel.js';
import { API_ORIGIN, PROXY_APP_ID, ACL_OPTIONS } from '../constants.js';

const STEP = Object.freeze({
  LOADING: Symbol('loading'),
  DETAILS: Symbol('details'),
  INSTALL: Symbol('install'),
});

const appsModel = AppsModel.create();
const domainsModel = DomainsModel.create();
const usersModel = UsersModel.create();
const groupsModel = GroupsModel.create();

const subscriptionRequiredDialog = inject('subscriptionRequiredDialog');
const dashboardDomain = inject('dashboardDomain');

// reactive
const busy = ref(false);
const formError = ref({});

// community { iconUrl, versionsUrl, manifest, publishState, creationDate, ts }
// appstore { id, iconUrl, appStoreId, manifest, creationDate, publishState }
const packageData = ref({});
const manifest = ref({});
const step = ref(STEP.DETAILS);
const dialog = useTemplateRef('dialogHandle');
const locationInput = useTemplateRef('locationInput');
const description = computed(() => marked.parse(manifest.value.description || ''));
const domains = ref([]);

const form = ref(null); // assigned via "Function Ref" because it is inside v-if
const isFormValid = ref(false);
async function checkValidity() {
  isFormValid.value = form.value ? form.value.checkValidity() : false;

  if (isFormValid.value) {
    if (accessRestrictionOption.value === ACL_OPTIONS.RESTRICTED && (accessRestrictionAcl.value.users.length === 0 && accessRestrictionAcl.value.groups.length === 0)) {
      isFormValid.value = true;
    }

    if (manifest.value.id === PROXY_APP_ID) {
      try {
        new URL(upstreamUri.value);
        // eslint-disable-next-line no-unused-vars
      } catch (e) {
        isFormValid.value = false;
      }
    }
  }
}
watch(form, () => { // trigger form validation when the ref becomes set
  setTimeout(checkValidity, 100);
});

const appMaxCountExceeded = ref(false);

function setStep(newStep) {
  if (newStep === STEP.INSTALL && appMaxCountExceeded.value) {
    dialog.value.close();
    subscriptionRequiredDialog.value.open();
    return;
  }

  step.value = newStep;
  if (newStep === STEP.INSTALL) {
    setTimeout(() => locationInput.value.$el.focus(), 500);
  }
}

// form data
const location = ref('');
const accessRestrictionOption = ref(ACL_OPTIONS.ANY);
const accessRestrictionAcl = ref({ users: [], groups: [] });
const domain = ref('');
const domainProvider = ref('');
const tcpPorts = ref({});
const udpPorts = ref({});
const secondaryDomains = ref({});
const upstreamUri = ref('');
const needsOverwriteDns = ref(false);
const users = ref([]);
const groups = ref([]);

function onDomainChange() {
  const tmp = domains.value.find(d => d.domain === domain.value);
  domainProvider.value = tmp ? tmp.provider : '';
}

async function onSubmit(overwriteDns) {
  if (!form.value.reportValidity()) return;

  formError.value = {};
  busy.value = true;

  const checkForDomains = [{
    domain: domain.value,
    subdomain: location.value,
  }];

  for (const d in secondaryDomains.value) checkForDomains.push({ domain: secondaryDomains.value[d].domain, subdomain: secondaryDomains.value[d].value });

  for (const d of checkForDomains) {
    const [error, result] = await domainsModel.checkRecords(d.domain, d.subdomain);
    if (error) {
      formError.value.location = error.body ? error.body.message : 'Internal error';
      busy.value = false;
      return console.error(error);
    }

    if (result.needsOverwrite && !overwriteDns) {
      busy.value = false;
      needsOverwriteDns.value = true;
      formError.value.dnsExists = `DNS record for ${d.subdomain}.${d.domain} already exists`;
      return;
    }
  }

  const config = {
    subdomain: location.value,
    domain: domain.value,
    accessRestriction: accessRestrictionOption.value === ACL_OPTIONS.ANY ? null : (accessRestrictionOption.value === ACL_OPTIONS.NOSSO ? null : accessRestrictionAcl.value)
  };

  if (overwriteDns) config.overwriteDns = true;

  if (manifest.value.optionalSso) config.sso = accessRestrictionOption.value !== ACL_OPTIONS.NOSSO;

  const finalPorts = {};
  for (const p in tcpPorts.value) {
    const port = tcpPorts.value[p];
    if (port.enabled) finalPorts[p] = port.value;
  }
  for (const p in udpPorts.value) {
    const port = udpPorts.value[p];
    if (port.enabled) finalPorts[p] = port.value;
  }
  config.ports = finalPorts;

  const finalSecondaryDomains = {};  for (const p in secondaryDomains.value) {
    finalSecondaryDomains[p] = {
      subdomain: secondaryDomains.value[p].value,
      domain: secondaryDomains.value[p].domain
    };
  }
  config.secondaryDomains = finalSecondaryDomains;

  if (manifest.value.id === PROXY_APP_ID) config.upstreamUri = upstreamUri.value;

  const [error, result] = await appsModel.install(packageData.value, config);

  if (!error) {
    dialog.value.close();
    if (manifest.value.postInstallMessage) localStorage['confirmPostInstall_' + result.id] = true;
    return window.location.href = `/#/app/${result.id}/info`;
  }

  busy.value = false;
  if (error.status === 409 && error.body.message.indexOf('port') !== -1) {
    const match = error.body.message.match(/.*port.(.*)/);
    formError.value.port = match ? parseInt(match[1]) : null;
  } else if (error.status === 409 && error.body.message.indexOf('primary location') !== -1) {
    formError.value.location = error.body.message;
  } else {
    formError.value.generic = error.body?.message || `Error installing app. Status code: ${error.status} . ${error.body}`;
    console.error('Failed to install:', error);
  }
}

function onClose() {
  setTimeout(() => step.value = STEP.DETAILS, 2000);
}

onMounted(async () => {
  let [error, result] = await usersModel.list();
  if (error) return console.error(error);
  result.forEach(u => { u.label = u.displayName || u.username || u.email });
  users.value = result;

  [error, result] = await groupsModel.list();
  if (error) return console.error(error);
  groups.value = result;
});

const screenshotsContainer = useTemplateRef('screenshotsContainer');
let currentScreenshotPos = 0;
function onScreenshotPrev() {
  if (currentScreenshotPos <= 0) currentScreenshotPos = manifest.value.mediaLinks.length-1;
  else --currentScreenshotPos;

  const elem = screenshotsContainer.value.children.item(currentScreenshotPos);
  elem.scrollIntoView({ behavior: 'smooth', inline: 'start', block: 'nearest' });
}

function onScreenshotNext() {
  if (currentScreenshotPos >= manifest.value.mediaLinks.length-1) currentScreenshotPos = 0;
  else ++currentScreenshotPos;

  const elem = screenshotsContainer.value.children.item(currentScreenshotPos);
  elem.scrollIntoView({ behavior: 'smooth', inline: 'start', block: 'nearest' });
}

defineExpose({
  open: async function(pd, appCountExceeded, domainList) {
    busy.value = false;
    step.value = STEP.LOADING;
    formError.value = {};

    packageData.value = pd;
    appMaxCountExceeded.value = appCountExceeded;
    manifest.value = packageData.value.manifest;
    location.value = '';
    accessRestrictionOption.value = ACL_OPTIONS.ANY;
    accessRestrictionAcl.value = { users: [], groups: [] };
    domainProvider.value = '';
    upstreamUri.value = '';
    needsOverwriteDns.value = '';

    domainList.forEach(d => {
      d.label = '.' + d.domain;
    });

    domains.value = domainList;

    // preselect with dashboard domain
    domain.value = domains.value.find(d => d.domain === dashboardDomain.value).domain;

    tcpPorts.value = manifest.value.tcpPorts;
    udpPorts.value = manifest.value.udpPorts;

    // ensure we have value property
    for (const p in tcpPorts.value) {
      tcpPorts.value[p].value = tcpPorts.value[p].value || tcpPorts.value[p].defaultValue;
      tcpPorts.value[p].enabled = tcpPorts.value[p].enabledByDefault ?? true;
    }
    for (const p in udpPorts.value) {
      udpPorts.value[p].value = udpPorts.value[p].value || udpPorts.value[p].defaultValue;
      udpPorts.value[p].enabled = udpPorts.value[p].enabledByDefault ?? true;
    }

    secondaryDomains.value = manifest.value.httpPorts;
    for (const p in secondaryDomains.value) {
      const port = secondaryDomains.value[p];
      port.value = port.defaultValue;
      port.domain = dashboardDomain.value;
    }

    currentScreenshotPos = 0;
    step.value = STEP.DETAILS;
    dialog.value.open();
  },
  close() {
    dialog.value.close();
  },
});

</script>

<template>
  <Dialog ref="dialogHandle" @close="onClose()" :show-x="step !== STEP.LOADING" style="width: unset;" :style="{ 'min-width': step !== STEP.LOADING ? 'min(450px, 95%)' : 'unset' }">
    <div v-if="step === STEP.LOADING" class="app-install-dialog-body">
      <Spinner class="pankow-spinner-large"/>
    </div>
    <div v-else class="app-install-dialog-body" :class="{ 'step-detail': step === STEP.DETAILS, 'step-install': step === STEP.INSTALL }">
      <div class="app-install-header">
        <div class="summary" v-if="packageData.manifest">
          <div class="title"><img class="icon pankow-no-desktop" style="width: 32px; height: 32px; margin-right: 10px" :src="packageData.iconUrl" v-fallback-image="API_ORIGIN + '/img/appicon_fallback.png'"/>{{ manifest.title }}</div>
          <div><a :href="manifest.website" target="_blank">{{ manifest.title }}</a> {{ packageData.manifest.upstreamVersion }} - {{ $t('app.updates.info.packageVersion') }} {{ packageData.manifest.version }}</div>
          <div v-if="packageData.versionsUrl"><a :href="packageData.manifest.packagerUrl" target="_blank">{{ packageData.manifest.packagerName }}</a></div>
            <div>{{ $t('appstore.installDialog.lastUpdated', { date: prettyDate(packageData.creationDate) }) }}</div>
          <div>{{ $t('appstore.installDialog.memoryRequirement', { size: prettyBinarySize(manifest.memoryLimit || (256 * 1024 * 1024)) }) }}</div>
        </div>
        <img class="icon pankow-no-mobile" :src="packageData.iconUrl" v-fallback-image="API_ORIGIN + '/img/appicon_fallback.png'"/>
      </div>
      <Transition name="slide-left" mode="out-in">
        <div v-if="step === STEP.DETAILS">
          <Button @click="setStep(STEP.INSTALL)" icon="fa-solid fa-circle-down">Install {{ manifest.title }}</Button>
          <div class="screenshots-container">
            <div class="screenshots" ref="screenshotsContainer">
              <img class="screenshot" v-for="image in manifest.mediaLinks" :key="image" :src="image"/>
            </div>
            <div class="screenshots-action screenshots-prev" @click="onScreenshotPrev" v-if="manifest.mediaLinks && manifest.mediaLinks.length > 1"><i class="fa fa-chevron-left"></i></div>
            <div class="screenshots-action screenshots-next" @click="onScreenshotNext" v-if="manifest.mediaLinks && manifest.mediaLinks.length > 1"><i class="fa fa-chevron-right"></i></div>
          </div>
          <div class="description" v-html="description"></div>
        </div>
        <div v-else-if="step === STEP.INSTALL">
          <div class="error-label" v-if="formError.generic">{{ formError.generic }}</div>
          <div class="error-label" v-if="formError.dnsExists">{{ formError.dnsExists }}</div>

          <form :ref="(el) => { form = el; }" @submit.prevent="onSubmit(false)" autocomplete="off" @input="checkValidity()">
            <fieldset :disabled="busy">
              <input style="display: none;" type="submit" :disabled="busy" />

              <FormGroup :class="{ 'has-error': formError.location }">
                <label for="location">{{ $t('appstore.installDialog.location') }}</label>
                <InputGroup>
                  <TextInput id="location" ref="locationInput" v-model="location" style="flex-grow: 1"/>
                  <SingleSelect v-model="domain" :options="domains" option-label="label" option-key="domain" @select="onDomainChange()" :search-threshold="10" required/>
                </InputGroup>
                <div class="warning-label" v-show="domainProvider === 'noop' || domainProvider === 'manual'" v-html="$t('appstore.installDialog.manualWarning', { location: ((location ? location + '.' : '') + domain) })"></div>
                <div class="error-label" v-if="formError.location">{{ formError.location }}</div>
              </FormGroup>

              <FormGroup v-for="(port, key) in secondaryDomains" :key="key">
                <label :for="'secondaryDomainInput' + key">{{ port.title }}</label>
                <small>{{ port.description }}</small>
                <InputGroup>
                  <TextInput :id="'secondaryDomainInput' + key" v-model="port.value" :placeholder="$t('appstore.installDialog.locationPlaceholder')" style="flex-grow: 1"/>
                  <SingleSelect v-model="port.domain" :options="domains" option-label="label" option-key="domain" required/>
                </InputGroup>
              </FormGroup>

              <FormGroup :class="{ 'has-error': formError.upstreamUri }" v-if="manifest.id === PROXY_APP_ID">
                <label for="upstreamUri">Upstream URI</label>
                <TextInput id="upstreamUri" v-model="upstreamUri" required/>
              </FormGroup>

              <PortBindings v-model:tcp="tcpPorts" v-model:udp="udpPorts" :error="formError" :domain-provider="domainProvider"/>
              <AccessControl v-model:option="accessRestrictionOption" v-model:acl="accessRestrictionAcl" :manifest="manifest" :users="users" :groups="groups" :installation="true"/>

              <div class="bottom-button-bar">
                <Button v-if="needsOverwriteDns" danger @click="onSubmit(true)" icon="fa-solid fa-circle-down" :disabled="busy || !isFormValid" :loading="busy">Install {{ manifest.title }} and overwrite DNS</Button>
                <Button v-else @click="onSubmit(false)" icon="fa-solid fa-circle-down" :disabled="busy || !isFormValid" :loading="busy">Install {{ manifest.title }}</Button>
              </div>
            </fieldset>
          </form>
        </div>
      </Transition>
    </div>
  </Dialog>
</template>

<style scoped>

.app-install-dialog-body {
  max-width: 100%;
  max-height: 100%;
  transition: all 0.25s;
}

.step-detail {
  width: 800px;
}

.step-install {
  width: 480px;
}

.app-install-header {
  display: flex;
  justify-content: space-between;
  margin-bottom: 10px;
}

.summary {
  display: flex;
  flex-direction: column;
  font-size: 14px;
  overflow: hidden;
}

.summary > div {
  text-wrap: nowrap;
}

.title {
  font-size: 32px;
  margin-bottom: 10px;
  display: flex;
  align-items: center;
}

.icon {
  width: 128px;
  height: 128px;
  object-fit: contain;
  margin-right: 20px;
}

.description {
  margin: 0 4px;
}

.screenshots-container {
  position: relative;
  overflow: hidden;
}

.screenshots {
  margin: 10px 4px;
  display: flex;
  gap: 20px;
  width: 100%;
  padding: 10px 0;
  scroll-snap-type: x mandatory;
  overflow: auto;
  scrollbar-width: none
}

.screenshot {
  display: inline-block;
  border-radius: var(--pankow-border-radius);
  height: 300px;
  object-fit: contain;
  scroll-snap-align: center;
  scroll-snap-stop: always;
}

.screenshots-action {
  color: white;
  position: absolute;
  top: calc(50% - 40px);
  cursor: pointer;
  margin: 20px;
  width: 40px;
  height: 40px;
  border-radius: 40px;
  padding: 10px;
  display: flex;
  align-items: center;
  justify-content: center;
  opacity: 0;
  transition: all 200ms;
  background-color: rgba(128,128,128,0.5);
}

.screenshots-container:hover .screenshots-action {
  opacity: 1;
}

.screenshots-action:hover {
  background-color: rgba(128,128,128,0.8);
}

.screenshots-prev {
 left: 0;
}

.screenshots-next {
 right: 0;
}

.bottom-button-bar {
  display: flex;
  justify-content: right;
  margin-top: 20px;
  margin-bottom: 0;
}

</style>
