import { configureMonacoYaml } from 'https://cdn.jsdelivr.net/npm/monaco-yaml@5.1.1/+esm';

if (!localStorage.getItem('info_shown')) {
  document.querySelector('#info-dialog').showModal();
  localStorage.setItem('info_shown', 'true');
}

let api_url = localStorage.getItem('api_url') ?? '';
let menu_url = localStorage.getItem('menu_url') ?? '';
let api_token = localStorage.getItem('api_token') ?? '';

/**
 * Get all entities in HomeAssistant.
 * @returns {Promise<Record<string, string>>} [id, name]
 */
async function get_entities() {
  try {
    const res = await fetch(api_url + '/template', {
      method: 'POST',
      headers: {
        Authorization: `Bearer ${api_token}`,
      },
      mode: 'cors',
      body: `{"template":"[{% for entity in states %}[\\"{{ entity.entity_id }}\\",\\"{{ entity.name }}\\"]{% if not loop.last %},{% endif %}{% endfor %}]"}`,
    });
    if (res.status == 401 || res.status == 403) {
      document.querySelector('#api_token').classList.add('invalid');
      return {};
    }
    document.querySelector('#api_url').classList.remove('invalid');
    document.querySelector('#api_token').classList.remove('invalid');
    return Object.fromEntries(await res.json());
  } catch {
    document.querySelector('#api_url').classList.add('invalid');
    return {};
  }
}

/**
 * Get all devices in HomeAssistant.
 * @returns {Promise<Record<string, string>>} [id, name]
 */
async function get_devices() {
  try {
    const res = await fetch(api_url + '/template', {
      method: 'POST',
      headers: {
        Authorization: `Bearer ${api_token}`,
      },
      mode: 'cors',
      body: `{"template":"{% set devices = states | map(attribute='entity_id') | map('device_id') | unique | reject('eq', None) | list %}[{% for device in devices %}[\\"{{ device }}\\",\\"{{ device_attr(device, 'name') }}\\"]{% if not loop.last %},{% endif %}{% endfor %}]"}`,
    });
    if (res.status == 401 || res.status == 403) {
      document.querySelector('#api_token').classList.add('invalid');
      return {};
    }
    document.querySelector('#api_url').classList.remove('invalid');
    document.querySelector('#api_token').classList.remove('invalid');
    return Object.fromEntries(await res.json());
  } catch {
    document.querySelector('#api_url').classList.add('invalid');
    return {};
  }
}

/**
 * Get all areas in HomeAssistant.
 * @returns {Promise<Record<string, string>>} [id, name]
 */
async function get_areas() {
  try {
    const res = await fetch(api_url + '/template', {
      method: 'POST',
      headers: {
        Authorization: `Bearer ${api_token}`,
      },
      mode: 'cors',
      body: `{"template":"[{% for area in areas() %}[\\"{{ area }}\\",\\"{{ area_name(area) }}\\"]{% if not loop.last %},{% endif %}{% endfor %}]"}`,
    });
    if (res.status == 401 || res.status == 403) {
      document.querySelector('#api_token').classList.add('invalid');
      return {};
    }
    document.querySelector('#api_url').classList.remove('invalid');
    document.querySelector('#api_token').classList.remove('invalid');
    return Object.fromEntries(await res.json());
  } catch {
    document.querySelector('#api_url').classList.add('invalid');
    return {};
  }
}

/**
 * Get all services in HomeAssistant.
 * @returns {Promise<[string, { name: string; description: string; fields:
 *     Record<string, { name: string; description: string; example: string;
 *     selector: unknown; required?: boolean }> }][]>} [id, data]
 */
async function get_services() {
  try {
    const res = await fetch(api_url + '/services', {
      method: 'GET',
      headers: {
        Authorization: `Bearer ${api_token}`,
      },
      mode: 'cors',
    });
    if (res.status == 401 || res.status == 403) {
      document.querySelector('#api_token').classList.add('invalid');
      return {};
    }
    document.querySelector('#api_url').classList.remove('invalid');
    document.querySelector('#api_token').classList.remove('invalid');
    const data = await res.json();
    const services = [];
    for (const d of data) {
      for (const service in d.services) {
        services.push([`${d.domain}.${service}`, d.services[service]]);
      }
    }
    return services;
  } catch {
    document.querySelector('#api_url').classList.add('invalid');
    return [];
  }
}

/**
 * Get schema from GitHub.
 * @returns {Promise<{}>}
 */
async function get_schema() {
  const res = await fetch(
    'https://raw.githubusercontent.com/house-of-abbey/GarminHomeAssistant/main/config.schema.json'
  );
  return res.json();
}

/**
 * Generate schema for HomeAssistant.
 * @param {Record<string, string>} entities
 * @param {Record<string, string>} devices
 * @param {Record<string, string>} areas
 * @param {[string, { name: string; description: string; fields:
 *     Record<string, { name: string; description: string; example: string;
 *     selector: unknown; required?: boolean }> }][]} services
 * @param {{}} schema
 * @returns {Promise<{}>}
 */
async function generate_schema(entities, devices, areas, services, schema) {
  schema.$defs.entity = {
    enum: Object.keys(entities),
  };
  schema.$defs.device = {
    enum: Object.keys(devices),
  };
  schema.$defs.area = {
    enum: Object.keys(areas),
  };

  const oneOf = [];
  for (const [id, data] of services) {
    const i_properties = {
      service: {
        title: data.name,
        description: data.description,
        const: id,
      },
      data: {
        type: 'object',
        properties: {},
        additionalProperties: false,
      },
    };
    const required = [];
    for (const [field, f] of Object.entries(data.fields)) {
      i_properties.data.properties[field] = {
        title: f.name,
        description: f.description,
        example: f.example,
      };
      if (f.required) {
        required.push(field);
      }

      const selector = f.selector;
      if (selector) {
        if (Object.hasOwn(selector, 'action')) {
          i_properties.data.properties[field].type = 'array';
          i_properties.data.properties[field].items = {
            $ref: '#/$defs/tap_action',
          };
        } else if (Object.hasOwn(selector, 'area')) {
          if (selector.area?.multiple) {
            i_properties.data.properties[field].type = 'array';
            i_properties.data.properties[field].items = {
              $ref: '#/$defs/area',
            };
          } else {
            i_properties.data.properties[field].$ref = '#/$defs/area';
          }
        } else if (Object.hasOwn(selector, 'boolean')) {
          i_properties.data.properties[field].type = 'boolean';
        } else if (Object.hasOwn(selector, 'number')) {
          i_properties.data.properties[field].type = 'number';
          i_properties.data.properties[field].minimum = selector.number?.min;
          i_properties.data.properties[field].maximum = selector.number?.max;
          i_properties.data.properties[field].multipleOf =
            selector.number?.step;
        } else if (Object.hasOwn(selector, 'color_temp')) {
          i_properties.data.properties[field].type = 'number';
          i_properties.data.properties[field].minimum =
            selector.color_temp?.min;
          i_properties.data.properties[field].maximum =
            selector.color_temp?.max;
          i_properties.data.properties[field].multipleOf =
            selector.color_temp?.step;
        } else if (Object.hasOwn(selector, 'date')) {
          i_properties.data.properties[field].type = 'string';
          i_properties.data.properties[field].pattern =
            '^\\d{4}-\\d{2}-\\d{2}$';
        } else if (Object.hasOwn(selector, 'datetime')) {
          i_properties.data.properties[field].type = 'string';
          i_properties.data.properties[field].pattern =
            '^\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}(\\:\\d{2})?$';
        } else if (Object.hasOwn(selector, 'time')) {
          i_properties.data.properties[field].type = 'string';
          i_properties.data.properties[field].pattern =
            '^\\d{2}:\\d{2}(\\:\\d{2})?$';
        } else if (Object.hasOwn(selector, 'device')) {
          if (selector.device?.multiple) {
            i_properties.data.properties[field].type = 'array';
            i_properties.data.properties[field].items = {
              $ref: '#/$defs/device',
            };
          } else {
            i_properties.data.properties[field].$ref = '#/$defs/device';
          }
        } else if (Object.hasOwn(selector, 'entity')) {
          if (selector.entity?.multiple) {
            i_properties.data.properties[field].type = 'array';
            i_properties.data.properties[field].items = {
              $ref: '#/$defs/entity',
            };
          } else {
            i_properties.data.properties[field].$ref = '#/$defs/entity';
          }
        } else if (Object.hasOwn(selector, 'icon')) {
          i_properties.data.properties[field].type = 'string';
          i_properties.data.properties[field].pattern = '^[^.]+:[^.]+$';
        } else if (Object.hasOwn(selector, 'location')) {
          i_properties.data.properties[field].type = 'object';
          i_properties.data.properties[field].properties = {
            longitude: {
              type: 'number',
            },
            latitude: {
              type: 'number',
            },
            radius: {
              type: 'number',
              minimum: 0,
            },
          };
        } else if (Object.hasOwn(selector, 'color_rgb')) {
          i_properties.data.properties[field].type = 'array';
          i_properties.data.properties[field].prefixItems = [
            {
              type: 'number',
              minimum: 0,
              maximum: 255,
              multipleOf: 1,
            },
            {
              type: 'number',
              minimum: 0,
              maximum: 255,
              multipleOf: 1,
            },
            {
              type: 'number',
              minimum: 0,
              maximum: 255,
              multipleOf: 1,
            },
          ];
        } else if (Object.hasOwn(selector, 'select')) {
          const oneOf2 = [];
          if (selector.select?.options) {
            for (let o of selector.select.options) {
              if (typeof o == 'string') {
                oneOf2.push({
                  const: o,
                });
              } else {
                oneOf2.push({
                  const: o.value || '',
                });
              }
            }
          }
          if (selector.select?.custom) {
            oneOf2.push({
              type: 'string',
            });
          }
          if (selector.select?.multiple) {
            i_properties.data.properties[field].type = 'array';
            i_properties.data.properties[field].items = {
              oneOf: oneOf2,
            };
          } else {
            i_properties.data.properties[field].oneOf = oneOf2;
          }
        } else if (Object.hasOwn(selector, 'state' in selector || 'template')) {
          i_properties.data.properties[field].type = 'string';
        } else if (Object.hasOwn(selector, 'text')) {
          let pattern;
          const p = selector.text?.type;
          if (p == 'color') {
            pattern = '^#[0-9a-fA-F]{6}$';
          } else if (p == 'date') {
            pattern = '^\\d{4}-\\d{2}-\\d{2}$';
          } else if (p == 'datetime-local') {
            pattern = '^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}$';
          } else if (p == 'email') {
            pattern =
              '^([^\\x00-\\x20\\x22\\x28\\x29\\x2c\\x2e\\x3a-\\x3c\\x3e\\x40\\x5b-\\x5d\\x7f-\\xff]+|\\x22([^\\x0d\\x22\\x5c\\x80-\\xff]|\\x5c[\\x00-\\x7f])*\\x22)(\\x2e([^\\x00-\\x20\\x22\\x28\\x29\\x2c\\x2e\\x3a-\\x3c\\x3e\\x40\\x5b-\\x5d\\x7f-\\xff]+|\\x22([^\\x0d\\x22\\x5c\\x80-\\xff]|\\x5c[\\x00-\\x7f])*\\x22))*\\x40([^\\x00-\\x20\\x22\\x28\\x29\\x2c\\x2e\\x3a-\\x3c\\x3e\\x40\\x5b-\\x5d\\x7f-\\xff]+|\\x5b([^\\x0d\\x5b-\\x5d\\x80-\\xff]|\\x5c[\\x00-\\x7f])*\\x5d)(\\x2e([^\\x00-\\x20\\x22\\x28\\x29\\x2c\\x2e\\x3a-\\x3c\\x3e\\x40\\x5b-\\x5d\\x7f-\\xff]+|\\x5b([^\\x0d\\x5b-\\x5d\\x80-\\xff]|\\x5c[\\x00-\\x7f])*\\x5d))*$';
          } else if (p == 'month') {
            pattern = '^\\d{4}-\\d{2}$';
          } else if (p == 'number') {
            pattern = '^d*.?d+$';
          } else if (p == 'time') {
            pattern = '^\\d{2}:\\d{2}$';
          } else if (p == 'url') {
            pattern =
              "^[a-z](?:[-a-z0-9\\+\\.])*:(?:\\/\\/(?:(?:%[0-9a-f][0-9a-f]|[-a-z0-9\\._~!\\$&'\\(\\)\\*\\+,;=:\\xA0-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFEF]|[\\uD800-\\uD83E\\uD840-\\uD87E\\uD880-\\uD8BE\\uD8C0-\\uD8FE\\uD900-\\uD93E\\uD940-\\uD97E\\uD980-\\uD9BE\\uD9C0-\\uD9FE\\uDA00-\\uDA3E\\uDA40-\\uDA7E\\uDA80-\\uDABE\\uDAC0-\\uDAFE\\uDB00-\\uDB3E\\uDB44-\\uDB7E][\\uDC00-\\uDFFF]|[\\uD83F\\uD87F\\uD8BF\\uD8FF\\uD93F\\uD97F\\uD9BF\\uD9FF\\uDA3F\\uDA7F\\uDABF\\uDAFF\\uDB3F\\uDB7F][\\uDC00-\\uDFFD])*@)?(?:\\[(?:(?:(?:[0-9a-f]{1,4}:){6}(?:[0-9a-f]{1,4}:[0-9a-f]{1,4}|(?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])(?:\\.(?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])){3})|::(?:[0-9a-f]{1,4}:){5}(?:[0-9a-f]{1,4}:[0-9a-f]{1,4}|(?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])(?:\\.(?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])){3})|(?:[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){4}(?:[0-9a-f]{1,4}:[0-9a-f]{1,4}|(?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])(?:\\.(?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])){3})|(?:[0-9a-f]{1,4}:[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){3}(?:[0-9a-f]{1,4}:[0-9a-f]{1,4}|(?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])(?:\\.(?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])){3})|(?:(?:[0-9a-f]{1,4}:){0,2}[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){2}(?:[0-9a-f]{1,4}:[0-9a-f]{1,4}|(?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])(?:\\.(?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])){3})|(?:(?:[0-9a-f]{1,4}:){0,3}[0-9a-f]{1,4})?::[0-9a-f]{1,4}:(?:[0-9a-f]{1,4}:[0-9a-f]{1,4}|(?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])(?:\\.(?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])){3})|(?:(?:[0-9a-f]{1,4}:){0,4}[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:[0-9a-f]{1,4}|(?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])(?:\\.(?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])){3})|(?:(?:[0-9a-f]{1,4}:){0,5}[0-9a-f]{1,4})?::[0-9a-f]{1,4}|(?:(?:[0-9a-f]{1,4}:){0,6}[0-9a-f]{1,4})?::)|v[0-9a-f]+[-a-z0-9\\._~!\\$&'\\(\\)\\*\\+,;=:]+)\\]|(?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])(?:\\.(?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])){3}|(?:%[0-9a-f][0-9a-f]|[-a-z0-9\\._~!\\$&'\\(\\)\\*\\+,;=@\\xA0-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFEF]|[\\uD800-\\uD83E\\uD840-\\uD87E\\uD880-\\uD8BE\\uD8C0-\\uD8FE\\uD900-\\uD93E\\uD940-\\uD97E\\uD980-\\uD9BE\\uD9C0-\\uD9FE\\uDA00-\\uDA3E\\uDA40-\\uDA7E\\uDA80-\\uDABE\\uDAC0-\\uDAFE\\uDB00-\\uDB3E\\uDB44-\\uDB7E][\\uDC00-\\uDFFF]|[\\uD83F\\uD87F\\uD8BF\\uD8FF\\uD93F\\uD97F\\uD9BF\\uD9FF\\uDA3F\\uDA7F\\uDABF\\uDAFF\\uDB3F\\uDB7F][\\uDC00-\\uDFFD])*)(?::[0-9]*)?(?:\\/(?:(?:%[0-9a-f][0-9a-f]|[-a-z0-9\\._~!\\$&'\\(\\)\\*\\+,;=:@\\xA0-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFEF]|[\\uD800-\\uD83E\\uD840-\\uD87E\\uD880-\\uD8BE\\uD8C0-\\uD8FE\\uD900-\\uD93E\\uD940-\\uD97E\\uD980-\\uD9BE\\uD9C0-\\uD9FE\\uDA00-\\uDA3E\\uDA40-\\uDA7E\\uDA80-\\uDABE\\uDAC0-\\uDAFE\\uDB00-\\uDB3E\\uDB44-\\uDB7E][\\uDC00-\\uDFFF]|[\\uD83F\\uD87F\\uD8BF\\uD8FF\\uD93F\\uD97F\\uD9BF\\uD9FF\\uDA3F\\uDA7F\\uDABF\\uDAFF\\uDB3F\\uDB7F][\\uDC00-\\uDFFD]))*)*|\\/(?:(?:(?:(?:%[0-9a-f][0-9a-f]|[-a-z0-9\\._~!\\$&'\\(\\)\\*\\+,;=:@\\xA0-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFEF]|[\\uD800-\\uD83E\\uD840-\\uD87E\\uD880-\\uD8BE\\uD8C0-\\uD8FE\\uD900-\\uD93E\\uD940-\\uD97E\\uD980-\\uD9BE\\uD9C0-\\uD9FE\\uDA00-\\uDA3E\\uDA40-\\uDA7E\\uDA80-\\uDABE\\uDAC0-\\uDAFE\\uDB00-\\uDB3E\\uDB44-\\uDB7E][\\uDC00-\\uDFFF]|[\\uD83F\\uD87F\\uD8BF\\uD8FF\\uD93F\\uD97F\\uD9BF\\uD9FF\\uDA3F\\uDA7F\\uDABF\\uDAFF\\uDB3F\\uDB7F][\\uDC00-\\uDFFD]))+)(?:\\/(?:(?:%[0-9a-f][0-9a-f]|[-a-z0-9\\._~!\\$&'\\(\\)\\*\\+,;=:@\\xA0-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFEF]|[\\uD800-\\uD83E\\uD840-\\uD87E\\uD880-\\uD8BE\\uD8C0-\\uD8FE\\uD900-\\uD93E\\uD940-\\uD97E\\uD980-\\uD9BE\\uD9C0-\\uD9FE\\uDA00-\\uDA3E\\uDA40-\\uDA7E\\uDA80-\\uDABE\\uDAC0-\\uDAFE\\uDB00-\\uDB3E\\uDB44-\\uDB7E][\\uDC00-\\uDFFF]|[\\uD83F\\uD87F\\uD8BF\\uD8FF\\uD93F\\uD97F\\uD9BF\\uD9FF\\uDA3F\\uDA7F\\uDABF\\uDAFF\\uDB3F\\uDB7F][\\uDC00-\\uDFFD]))*)*)?|(?:(?:(?:%[0-9a-f][0-9a-f]|[-a-z0-9\\._~!\\$&'\\(\\)\\*\\+,;=:@\\xA0-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFEF]|[\\uD800-\\uD83E\\uD840-\\uD87E\\uD880-\\uD8BE\\uD8C0-\\uD8FE\\uD900-\\uD93E\\uD940-\\uD97E\\uD980-\\uD9BE\\uD9C0-\\uD9FE\\uDA00-\\uDA3E\\uDA40-\\uDA7E\\uDA80-\\uDABE\\uDAC0-\\uDAFE\\uDB00-\\uDB3E\\uDB44-\\uDB7E][\\uDC00-\\uDFFF]|[\\uD83F\\uD87F\\uD8BF\\uD8FF\\uD93F\\uD97F\\uD9BF\\uD9FF\\uDA3F\\uDA7F\\uDABF\\uDAFF\\uDB3F\\uDB7F][\\uDC00-\\uDFFD]))+)(?:\\/(?:(?:%[0-9a-f][0-9a-f]|[-a-z0-9\\._~!\\$&'\\(\\)\\*\\+,;=:@\\xA0-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFEF]|[\\uD800-\\uD83E\\uD840-\\uD87E\\uD880-\\uD8BE\\uD8C0-\\uD8FE\\uD900-\\uD93E\\uD940-\\uD97E\\uD980-\\uD9BE\\uD9C0-\\uD9FE\\uDA00-\\uDA3E\\uDA40-\\uDA7E\\uDA80-\\uDABE\\uDAC0-\\uDAFE\\uDB00-\\uDB3E\\uDB44-\\uDB7E][\\uDC00-\\uDFFF]|[\\uD83F\\uD87F\\uD8BF\\uD8FF\\uD93F\\uD97F\\uD9BF\\uD9FF\\uDA3F\\uDA7F\\uDABF\\uDAFF\\uDB3F\\uDB7F][\\uDC00-\\uDFFD]))*)*|(?!(?:%[0-9a-f][0-9a-f]|[-a-z0-9\\._~!\\$&'\\(\\)\\*\\+,;=:@\\xA0-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFEF]|[\\uD800-\\uD83E\\uD840-\\uD87E\\uD880-\\uD8BE\\uD8C0-\\uD8FE\\uD900-\\uD93E\\uD940-\\uD97E\\uD980-\\uD9BE\\uD9C0-\\uD9FE\\uDA00-\\uDA3E\\uDA40-\\uDA7E\\uDA80-\\uDABE\\uDAC0-\\uDAFE\\uDB00-\\uDB3E\\uDB44-\\uDB7E][\\uDC00-\\uDFFF]|[\\uD83F\\uD87F\\uD8BF\\uD8FF\\uD93F\\uD97F\\uD9BF\\uD9FF\\uDA3F\\uDA7F\\uDABF\\uDAFF\\uDB3F\\uDB7F][\\uDC00-\\uDFFD])))(?:\\?(?:%[0-9a-f][0-9a-f]|[-a-z0-9\\._~!\\$&'\\(\\)\\*\\+,;=:@\\/\\?\\xA0-\\uD7FF\\uE000-\\uFDCF\\uFDF0-\\uFFEF]|[\\uD800-\\uD83E\\uD840-\\uD87E\\uD880-\\uD8BE\\uD8C0-\\uD8FE\\uD900-\\uD93E\\uD940-\\uD97E\\uD980-\\uD9BE\\uD9C0-\\uD9FE\\uDA00-\\uDA3E\\uDA40-\\uDA7E\\uDA80-\\uDABE\\uDAC0-\\uDAFE\\uDB00-\\uDB3E\\uDB44-\\uDB7E\\uDB80-\\uDBBE\\uDBC0-\\uDBFE][\\uDC00-\\uDFFF]|[\\uD83F\\uD87F\\uD8BF\\uD8FF\\uD93F\\uD97F\\uD9BF\\uD9FF\\uDA3F\\uDA7F\\uDABF\\uDAFF\\uDB3F\\uDB7F\\uDBBF\\uDBFF][\\uDC00-\\uDFFD])*)?(?:\\#(?:%[0-9a-f][0-9a-f]|[-a-z0-9\\._~!\\$&'\\(\\)\\*\\+,;=:@\\/\\?\\xA0-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFEF]|[\\uD800-\\uD83E\\uD840-\\uD87E\\uD880-\\uD8BE\\uD8C0-\\uD8FE\\uD900-\\uD93E\\uD940-\\uD97E\\uD980-\\uD9BE\\uD9C0-\\uD9FE\\uDA00-\\uDA3E\\uDA40-\\uDA7E\\uDA80-\\uDABE\\uDAC0-\\uDAFE\\uDB00-\\uDB3E\\uDB44-\\uDB7E][\\uDC00-\\uDFFF]|[\\uD83F\\uD87F\\uD8BF\\uD8FF\\uD93F\\uD97F\\uD9BF\\uD9FF\\uDA3F\\uDA7F\\uDABF\\uDAFF\\uDB3F\\uDB7F][\\uDC00-\\uDFFD])*)?$";
          } else if (p == 'week') {
            pattern = '^\\d{4}-W\\d{2}$';
          }
          if (selector.text?.multiple) {
            i_properties.data.properties[field].type = 'array';
            i_properties.data.properties[field].items = {
              type: 'string',
              pattern: pattern,
            };
          } else {
            i_properties.data.properties[field].type = 'string';
            i_properties.data.properties[field].pattern = pattern;
          }
        }
      }
      if (required.length > 0) {
        i_properties.data.required = required;
      }
    }
    oneOf.push({
      title: data.name,
      description: data.description,
      properties: i_properties,
    });
  }
  schema.$defs.tap_action = {
    type: 'object',
    oneOf: oneOf,
    properties: {
      service: {
        type: 'string',
      },
      confirm: {
        $ref: '#/$defs/confirm',
      },
      data: {
        type: 'object',
        properties: {},
      },
    },
  };
  delete schema.$defs.tap.properties.service;
  delete schema.$schema;

  return schema;
}

function get(d, p) {
  for (let i = 0; i < p.length; i++) {
    d = d[p[i]];
  }
  return d;
}

/**
 * @param {{ text: string; color: string }} options
 */
function toast({ text, color }) {
  const t = Toastify({
    text,
    gravity: 'bottom', // `top` or `bottom`
    position: 'right', // `left`, `center` or `right`
    stopOnFocus: true, // Prevents dismissing of toast on hover
    // close: true,
    style: {
      background: 'var(--ctp-mocha-base)',
      outline: '1px solid ' + (color ?? 'var(--ctp-mocha-blue)'),
    },
  });
  t.showToast();
  return t;
}

/** @type {Awaited<ReturnType<typeof get_entities>>} */
let entities;
/** @type {Awaited<ReturnType<typeof get_devices>>} */
let devices;
/** @type {Awaited<ReturnType<typeof get_areas>>} */
let areas;
/** @type {Awaited<ReturnType<typeof get_services>>} */
let services;
let schema;
async function loadSchema() {
  [entities, devices, areas, services, schema] = await Promise.all([
    get_entities(),
    get_devices(),
    get_areas(),
    get_services(),
    get_schema(),
  ]);
  if (window.makeMarkers) {
    window.makeMarkers();
  }
  try {
    schema = await generate_schema(entities, devices, areas, services, schema);
  } catch {}
  console.log(schema);
  if (window.m && window.modelUri) {
    // configure the JSON language support with schemas and schema associations
    window.m.languages.json.jsonDefaults.setDiagnosticsOptions({
      validate: true,
      schemas: [
        {
          uri: 'https://raw.githubusercontent.com/house-of-abbey/GarminHomeAssistant/main/config.schema.json',
          fileMatch: [window.modelUri.toString()],
          schema,
        },
      ],
    });
  }
}
loadSchema();

// require is provided by loader.min.js.
require.config({
  paths: {
    vs: 'https://www.unpkg.com/monaco-editor@0.45.0/min/vs',
  },
});
require(['vs/editor/editor.main'], async () => {
  window.m = monaco;
  var modelUri = monaco.Uri.parse('/config/www/garmin/menu.json'); // a made up unique URI for our model
  window.modelUri = modelUri;

  if (schema) {
    // configure the JSON language support with schemas and schema associations
    monaco.languages.json.jsonDefaults.setDiagnosticsOptions({
      validate: true,
      schemas: [
        {
          uri: 'https://raw.githubusercontent.com/house-of-abbey/GarminHomeAssistant/main/config.schema.json',
          fileMatch: [modelUri.toString()],
          schema,
        },
      ],
    });
  }

  configureMonacoYaml(monaco);

  document.querySelector('#api_url').value = api_url;
  document.querySelector('#menu_url').value = menu_url;
  document.querySelector('#api_token').value = api_token;

  document.querySelector('#troubleshooting').addEventListener('click', (e) => {
    document.querySelector('#troubleshooting-dialog').showModal();
  });
  document.querySelector('#info').addEventListener('click', (e) => {
    document.querySelector('#info-dialog').showModal();
  });

  document.querySelector('#test-api').addEventListener('click', async (e) => {
    try {
      document.querySelector('#test-api-response').innerText = 'Testing...';
      const res = await fetch(api_url + '/', {
        headers: {
          Authorization: `Bearer ${api_token}`,
        },
        cache: 'no-cache',
        mode: 'cors',
      });
      const text = await res.text();
      if (res.status == 200) {
        document.querySelector('#test-api-response').innerText =
          JSON.parse(text).message;
        document.querySelector('#api_token').classList.remove('invalid');
        document.querySelector('#api_url').classList.remove('invalid');
      } else if (res.status == 400) {
        document.querySelector('#api_url').classList.add('invalid');
        try {
          document.querySelector('#test-api-response').innerText =
            JSON.parse(text).message;
        } catch {
          document.querySelector('#test-api-response').innerText = text;
        }
      } else if (res.status == 401 || res.status == 403) {
        document.querySelector('#api_token').classList.add('invalid');
        document.querySelector('#test-api-response').innerText =
          'Invalid token.';
      } else {
        document.querySelector('#test-api-response').innerText = text;
      }
    } catch (e) {
      document.querySelector('#test-api-response').innerText =
        'Check CORS settings on HomeAssistant server.';
      document.querySelector('#api_token').classList.add('invalid');
    }
  });
  document.querySelector('#test-menu').addEventListener('click', async (e) => {
    try {
      document.querySelector('#test-menu-response').innerText = 'Testing...';
      const res = await fetch(menu_url, {
        cache: 'no-cache',
        mode: 'cors',
      });
      if (res.status == 200) {
        document.querySelector('#menu_url').classList.remove('invalid');
        document.querySelector('#test-menu-response').innerText = 'Available';
      } else if (res.status == 400) {
        document.querySelector('#menu_url').classList.add('invalid');
        document.querySelector('#test-menu-response').innerText =
          await res.text();
      } else {
        document.querySelector('#menu_url').classList.add('invalid');
        document.querySelector('#test-menu-response').innerText =
          await res.text();
      }
    } catch (e) {
      document.querySelector('#menu_url').classList.add('invalid');
      document.querySelector('#test-menu-response').innerText =
        'Check CORS settings on HomeAssistant server.';
    }
  });
  document.querySelector('#download').addEventListener('click', async (e) => {
    try {
      const t = toast({
        text: 'Downloading...',
      });
      const res = await fetch(menu_url, {
        cache: 'no-cache',
        mode: 'cors',
      });
      t.hideToast();
      if (res.status == 200) {
        document.querySelector('#menu_url').classList.remove('invalid');
        const text = await res.text();
        model.setValue(text);
        toast({
          text: 'Downloaded!',
          color: 'var(--ctp-mocha-green)',
        });
      } else {
        document.querySelector('#menu_url').classList.add('invalid');
        toast({
          text: await res.text(),
          color: 'var(--ctp-mocha-red)',
        });
      }
    } catch (e) {
      toast({
        text: 'Check CORS settings on HomeAssistant server.',
        color: 'var(--ctp-mocha-red)',
      });
      document.querySelector('#menu_url').classList.add('invalid');
    }
  });
  document.querySelector('#copy').addEventListener('click', async (e) => {
    navigator.clipboard.writeText(model.getValue());
    toast({
      text: 'Copied!',
      color: 'var(--ctp-mocha-green)',
    });
  });

  document.querySelector('#api_url').addEventListener('change', (e) => {
    api_url = e.target.value;
    localStorage.setItem('api_url', api_url);
    document.querySelector('#test-api-response').innerText = 'Check now!';
    loadSchema();
  });
  document.querySelector('#menu_url').addEventListener('change', (e) => {
    menu_url = e.target.value;
    localStorage.setItem('menu_url', menu_url);
    document.querySelector('#test-menu-response').innerText = 'Check now!';
    checkRemoteMenu();
  });
  document.querySelector('#api_token').addEventListener('change', (e) => {
    api_token = e.target.value;
    localStorage.setItem('api_token', api_token);
    document.querySelector('#api_token').classList.remove('invalid');
    document.querySelector('#test-api-response').innerText = 'Check now!';
    loadSchema();
  });
  checkRemoteMenu();

  async function checkRemoteMenu() {
    if (menu_url != '') {
      try {
        const remote = await fetch(menu_url, {
          cache: 'no-cache',
          mode: 'cors',
        });
        if (remote.status == 200) {
          document.querySelector('#menu_url').classList.remove('invalid');
          document.querySelector('#download').disabled = false;
          const text = await remote.text();
          if (model.getValue() === text) {
            document.querySelector('#menu_url').classList.remove('outofsync');
          } else {
            document.querySelector('#menu_url').classList.add('outofsync');
          }
        } else {
          document.querySelector('#menu_url').classList.add('invalid');
          document.querySelector('#download').disabled = true;
        }
      } catch {
        document.querySelector('#menu_url').classList.add('invalid');
        document.querySelector('#download').disabled = true;
      }
    } else {
      document.querySelector('#menu_url').classList.remove('invalid');
      document.querySelector('#download').disabled = true;
    }
  }

  setInterval(checkRemoteMenu, 30000);

  var model = monaco.editor.createModel(
    localStorage.getItem('json') ?? '{}',
    'json',
    modelUri
  );

  monaco.editor.defineTheme(
    'mocha',
    await fetch(
      'https://josephabbey.github.io/catppuccin-monaco/mocha.json'
    ).then((r) => r.json())
  );

  monaco.editor.colorizeElement(document.querySelector('#cors-settings'), {
    theme: 'mocha',
  });

  monaco.languages.registerCompletionItemProvider('json', {
    triggerCharacters: ['.'],
    provideCompletionItems: function (model, position) {
      // find out if we are completing a property in the 'dependencies' object.
      var textUntilPosition = model.getValueInRange({
        startLineNumber: 1,
        startColumn: 1,
        endLineNumber: position.lineNumber,
        endColumn: position.column,
      });
      var match = /"content"\s*:\s*"[^"]*[^\w]?\w+\.[^.\s{}()[\]'"]*$/.test(
        textUntilPosition
      );
      if (!match) {
        return { suggestions: [] };
      }
      var word = model.getWordUntilPosition(position);
      let i = word.word.length - 1;
      while (word.word[i] != '.') {
        i--;
      }
      do {
        i--;
      } while (
        i >= 0 &&
        (word.word[i] == '_' ||
          word.word[i].toUpperCase() != word.word[i].toLowerCase())
      );
      i++;
      var range = {
        startLineNumber: position.lineNumber,
        endLineNumber: position.lineNumber,
        startColumn: word.startColumn + i,
        endColumn: word.endColumn,
      };
      return {
        suggestions: Object.entries(entities).map(([entity, name]) => ({
          label: entity,
          kind: monaco.languages.CompletionItemKind.Variable,
          documentation: name,
          insertText: entity,
          range,
        })),
      };
    },
  });

  const editor = monaco.editor.create(document.getElementById('container'), {
    model: model,
    theme: 'mocha',
    automaticLayout: true,
    glyphMargin: true,
  });

  window.addEventListener('keydown', (e) => {
    if (e.key == 's' && e.ctrlKey) {
      e.preventDefault();
      model.setValue(
        JSON.stringify(JSON.parse(editor.getValue()), undefined, 2) + '\n'
      );
    }
  });

  var decorations = editor.createDecorationsCollection([]);

  let markers = [];

  const renderTemplate = editor.addCommand(
    0,
    async function (_, template) {
      const t = toast({
        text: 'Rendering template...',
      });
      try {
        const res = await fetch(api_url + '/template', {
          method: 'POST',
          headers: {
            Authorization: `Bearer ${api_token}`,
          },
          mode: 'cors',
          body: `{"template":"${template}"}`,
        });
        t.hideToast();
        if (res.status == 200) {
          toast({
            text: await res.text(),
            color: 'var(--ctp-mocha-green)',
          });
        } else if (res.status == 400) {
          toast({
            text: (await res.json()).message,
            color: 'var(--ctp-mocha-red)',
          });
        } else if (res.status == 401 || res.status == 403) {
          document.querySelector('#api_token').classList.add('invalid');
        } else {
          toast({
            text: await res.text(),
            color: 'var(--ctp-mocha-red)',
          });
        }
      } catch (e) {
        t.hideToast();
        toast({
          text: 'Check CORS settings on HomeAssistant server.',
          color: 'var(--ctp-mocha-red)',
        });
        document.querySelector('#api_url').classList.add('invalid');
      }
    },
    ''
  );

  const runAction = editor.addCommand(
    0,
    async function (_, action) {
      const service = action.tap_action.service.split('.');
      let data = action.tap_action.data;
      if (data) {
        data.entity_id = action.entity;
      } else {
        data = {
          entity_id: action.entity,
        };
      }
      const t = toast({
        text: 'Running action...',
      });
      try {
        const res = await fetch(
          api_url + '/services/' + service[0] + '/' + service[1],
          {
            method: 'POST',
            headers: {
              Authorization: `Bearer ${api_token}`,
            },
            mode: 'cors',
            body: JSON.stringify(data),
          }
        );
        t.hideToast();
        if (res.status == 200) {
          toast({
            text: 'Success',
            color: 'var(--ctp-mocha-green)',
          });
        } else if (res.status == 400) {
          const text = await res.text();
          try {
            toast({
              text: JSON.parse(text).message,
              color: 'var(--ctp-mocha-red)',
            });
          } catch {
            toast({
              text: text,
              color: 'var(--ctp-mocha-red)',
            });
          }
        } else if (res.status == 401 || res.status == 403) {
          document.querySelector('#api_token').classList.add('invalid');
        } else {
          toast({
            text: await res.text(),
            color: 'var(--ctp-mocha-red)',
          });
        }
        makeMarkers();
      } catch (e) {
        t.hideToast();
        toast({
          text: 'Check CORS settings on HomeAssistant server.',
          color: 'var(--ctp-mocha-red)',
        });
        document.querySelector('#api_url').classList.add('invalid');
      }
    },
    ''
  );

  const toggle = editor.addCommand(
    0,
    async function (_, item) {
      const entity = item.entity.split('.');
      const t = toast({
        text: 'Toggling...',
      });
      try {
        const res = await fetch(
          api_url + '/services/' + entity[0] + '/toggle',
          {
            method: 'POST',
            headers: {
              Authorization: `Bearer ${api_token}`,
            },
            mode: 'cors',
            body: JSON.stringify({
              entity_id: item.entity,
            }),
          }
        );
        t.hideToast();
        if (res.status == 200) {
          toast({
            text: 'Success',
            color: 'var(--ctp-mocha-green)',
          });
        } else if (res.status == 400) {
          const text = await res.text();
          try {
            toast({
              text: JSON.parse(text).message,
              color: 'var(--ctp-mocha-red)',
            });
          } catch {
            toast({
              text: text,
              color: 'var(--ctp-mocha-red)',
            });
          }
        } else if (res.status == 401 || res.status == 403) {
          document.querySelector('#api_token').classList.add('invalid');
        } else {
          toast({
            text: await res.text(),
            color: 'var(--ctp-mocha-red)',
          });
        }
        makeMarkers();
      } catch (e) {
        t.hideToast();
        toast({
          text: 'Check CORS settings on HomeAssistant server.',
          color: 'var(--ctp-mocha-red)',
        });
        document.querySelector('#api_url').classList.add('invalid');
      }
    },
    ''
  );

  async function makeMarkers() {
    try {
      const ast = json.parse(model.getValue());
      const data = JSON.parse(model.getValue());
      markers = [];
      const glyphs = [];
      async function testToggle(range, entity) {
        const res = await fetch(api_url + '/states/' + entity, {
          method: 'GET',
          headers: {
            Authorization: `Bearer ${api_token}`,
          },
          mode: 'cors',
        });
        const d = await res.json();
        if (d.state == 'on') {
          decorations.append([
            {
              range,
              options: {
                isWholeLine: true,
                glyphMarginClassName: 'toggle_on',
              },
            },
          ]);
        } else {
          decorations.append([
            {
              range,
              options: {
                isWholeLine: true,
                glyphMarginClassName: 'toggle_off',
              },
            },
          ]);
        }
      }
      const toggles = [];
      async function testTemplate(range, template) {
        const l = model.getValueInRange(range);
        let trim = 0;
        while (trim < l.length && l[trim] == ' ') {
          trim++;
        }
        trim++;
        const res = await fetch(api_url + '/template', {
          method: 'POST',
          headers: {
            Authorization: `Bearer ${api_token}`,
          },
          mode: 'cors',
          body: `{"template":"${template}"}`,
        });
        if (res.status == 200) {
          markers.push({
            message: await res.text(),
            severity: monaco.MarkerSeverity.Info,
            ...range,
            startColumn: trim,
          });
        } else if (res.status == 400) {
          markers.push({
            message: (await res.json()).message,
            severity: monaco.MarkerSeverity.Error,
            ...range,
            startColumn: trim,
          });
        } else if (res.status == 401 || res.status == 403) {
          document.querySelector('#api_token').classList.add('invalid');
        } else {
          markers.push({
            message: res.statusText,
            severity: monaco.MarkerSeverity.Error,
            ...range,
            startColumn: trim,
          });
        }
      }
      const templates = [];
      /**
       * @param {import('json-ast-comments').JsonAst |
       *     import('json-ast-comments').JsonProperty} node
       * @param {string[]} path
       */
      function recurse(node, path) {
        if (node.type === 'property') {
          if (node.key[0].value === 'content') {
            templates.push([
              {
                startLineNumber: node.key[0].range.start.line + 1,
                startColumn: 0,
                endLineNumber: node.value[0].range.end.line + 1,
                endColumn: 10000,
              },
              node.value[0].value,
            ]);
          } else if (entities != null && node.key[0].value === 'entity') {
            const range = {
              startLineNumber: node.key[0].range.start.line + 1,
              startColumn: 0,
              endLineNumber: node.value[0].range.end.line + 1,
              endColumn: 10000,
            };
            const l = model.getValueInRange(range);
            let trim = 0;
            while (trim < l.length && l[trim] == ' ') {
              trim++;
            }
            trim++;
            markers.push({
              message: entities[node.value[0].value] ?? 'Entity not found',
              severity: monaco.MarkerSeverity.Hint,
              ...range,
              startColumn: trim,
            });
          } else if (node.key[0].value === 'type') {
            if (node.value[0].value === 'toggle') {
              toggles.push([
                {
                  startLineNumber: node.key[0].range.start.line + 1,
                  startColumn: 0,
                  endLineNumber: node.value[0].range.end.line + 1,
                  endColumn: 10000,
                },
                get(data, path).entity,
              ]);
            } else {
              glyphs.push({
                range: {
                  startLineNumber: node.key[0].range.start.line + 1,
                  startColumn: 0,
                  endLineNumber: node.value[0].range.end.line + 1,
                  endColumn: 10000,
                },
                options: {
                  isWholeLine: true,
                  glyphMarginClassName: node.value[0].value,
                },
              });
            }
          } else {
            recurse(node.value[0], [...path, node.key[0].value]);
          }
        } else if (node.type === 'array') {
          for (let i = 0; i < node.members.length; i++) {
            recurse(node.members[i], [...path, i]);
          }
        } else if (node.type === 'object') {
          for (let member of node.members) {
            recurse(member, path);
          }
        }
      }
      recurse(ast.body[0], []);
      decorations.clear();
      decorations.append(glyphs);
      await Promise.all(templates.map((t) => testTemplate(...t)));
      toggles.forEach((t) => testToggle(...t));
      monaco.editor.setModelMarkers(model, 'template', markers);
    } catch {}
  }
  window.makeMarkers = makeMarkers;
  makeMarkers();

  model.onDidChangeContent(async function () {
    localStorage.setItem('json', model.getValue());
    makeMarkers();
  });

  monaco.languages.registerCodeLensProvider('json', {
    provideCodeLenses: function (model, token) {
      const lenses = [];
      try {
        const ast = json.parse(model.getValue());
        const data = JSON.parse(model.getValue());
        /**
         * @param {import('json-ast-comments').JsonAst |
         *     import('json-ast-comments').JsonProperty} node
         * @param {string[]} path
         */
        function recurse(node, path) {
          if (node.type === 'property') {
            if (node.key[0].value === 'tap_action') {
              const d = get(data, path);
              if (d.tap_action.service) {
                lenses.push({
                  range: {
                    startLineNumber: node.key[0].range.start.line + 1,
                    startColumn: 0,
                    endLineNumber: node.key[0].range.start.line + 1,
                    endColumn: 0,
                  },
                  id: Math.random().toString(36).substring(7),
                  command: {
                    id: runAction,
                    title: 'Run Action',
                    arguments: [d],
                  },
                });
              } else {
                recurse(node.value[0], [...path, node.key[0].value]);
              }
            } else if (node.key[0].value === 'content') {
              lenses.push({
                range: {
                  startLineNumber: node.key[0].range.start.line + 1,
                  startColumn: 0,
                  endLineNumber: node.key[0].range.start.line + 1,
                  endColumn: 0,
                },
                id: Math.random().toString(36).substring(7),
                command: {
                  id: renderTemplate,
                  title: 'Render Template',
                  arguments: [node.value[0].value],
                },
              });
            } else if (
              node.key[0].value === 'type' &&
              node.value[0].value === 'toggle'
            ) {
              lenses.push({
                range: {
                  startLineNumber: node.key[0].range.start.line + 1,
                  startColumn: 0,
                  endLineNumber: node.key[0].range.start.line + 1,
                  endColumn: 0,
                },
                id: Math.random().toString(36).substring(7),
                command: {
                  id: toggle,
                  title: 'Toggle',
                  arguments: [get(data, path)],
                },
              });
            } else {
              recurse(node.value[0], [...path, node.key[0].value]);
            }
          } else if (node.type === 'array') {
            for (let i = 0; i < node.members.length; i++) {
              recurse(node.members[i], [...path, i]);
            }
          } else if (node.type === 'object') {
            for (let member of node.members) {
              recurse(member, path);
            }
          }
        }
        recurse(ast.body[0], []);
      } catch {}
      return {
        lenses,
        dispose: () => {},
      };
    },
    resolveCodeLens: function (model, codeLens, token) {
      return codeLens;
    },
  });
});
