import { SonosManager, SonosDevice } from "@svrooij/sonos";
import axios from "axios";
import { parse } from "node-html-parser";
import { MusicService } from "@svrooij/sonos/lib/services";
import { head } from "underscore";
import logger from "./logger";
import { SOAP_PATH, STRINGS_ROUTE, PRESENTATION_MAP_ROUTE } from "./smapi";
import qs from "querystring";
import { URLBuilder } from "./url_builder";
import { LANG } from "./i8n";

export const SONOS_LANG: LANG[] = [
  "en-US",
  "da-DK",
  "de-DE",
  "es-ES",
  "fr-FR",
  "it-IT",
  "ja-JP",
  "nb-NO",
  "nl-NL",
  "pt-BR",
  "sv-SE",
  "zh-CN",
];

export const PRESENTATION_AND_STRINGS_VERSION =
  process.env["BNB_DEBUG"] === "true"
    ? `${Math.round(new Date().getTime() / 1000)}`
    : "23";

// NOTE: manifest requires https for the URL, otherwise you will get an error trying to register
export type Capability =
  | "search"
  | "trFavorites" // Favorites: Adding/Removing Tracks (deprecated)
  | "alFavorites" // Favorites: Adding/Removing Albums (deprecated)
  | "ucPlaylists" // User Content Playlists
  | "extendedMD" // Extended Metadata (More Menu, Info & Options)
  | "contextHeaders"
  | "authorizationHeader"
  | "logging" // Playback duration logging at track end (deprecated)
  | "manifest";

export const BONOB_CAPABILITIES: Capability[] = [
  "search",
  "ucPlaylists",
  "extendedMD",
  "logging",
];

export type Device = {
  name: string;
  group: string;
  ip: string;
  port: number;
};

export type Service = {
  name: string;
  sid: number;
  uri: string;
  secureUri: string;
  strings?: { uri?: string; version?: string };
  presentation?: { uri?: string; version?: string };
  pollInterval?: number;
  authType: "Anonymous" | "AppLink" | "DeviceLink" | "UserId";
};

export const stripTrailingSlash = (url: string) =>
  url.endsWith("/") ? url.substring(0, url.length - 1) : url;

export const bonobService = (
  name: string,
  sid: number,
  bonobUrl: URLBuilder,
  authType: "Anonymous" | "AppLink" | "DeviceLink" | "UserId" = "AppLink"
): Service => ({
  name,
  sid,
  uri: bonobUrl.append({ pathname: SOAP_PATH }).href(),
  secureUri: bonobUrl.append({ pathname: SOAP_PATH }).href(),
  strings: {
    uri: bonobUrl.append({ pathname: STRINGS_ROUTE }).href(),
    version: PRESENTATION_AND_STRINGS_VERSION,
  },
  presentation: {
    uri: bonobUrl.append({ pathname: PRESENTATION_MAP_ROUTE }).href(),
    version: PRESENTATION_AND_STRINGS_VERSION,
  },
  pollInterval: 1200,
  authType,
});

export interface Sonos {
  devices: () => Promise<Device[]>;
  services: () => Promise<Service[]>;
  remove: (sid: number) => Promise<boolean>;
  register: (service: Service) => Promise<boolean>;
}

export const SONOS_DISABLED: Sonos = {
  devices: () => Promise.resolve([]),
  services: () => Promise.resolve([]),
  remove: (_: number) => Promise.resolve(false),
  register: (_: Service) => Promise.resolve(false),
};

export const asService = (musicService: MusicService): Service => ({
  name: musicService.Name,
  sid: musicService.Id,
  uri: musicService.Uri,
  secureUri: musicService.SecureUri,
  strings: {
    uri: musicService.Presentation?.Strings?.Uri,
    version: musicService.Presentation?.Strings?.Version,
  },
  presentation: {
    uri: musicService.Presentation?.PresentationMap?.Uri,
    version: musicService.Presentation?.PresentationMap?.Version,
  },
  pollInterval: musicService.Policy.PollInterval,
  authType: musicService.Policy.Auth,
});

export const asDevice = (sonosDevice: SonosDevice): Device => ({
  name: sonosDevice.Name,
  group: sonosDevice.GroupName || "",
  ip: sonosDevice.Host,
  port: sonosDevice.Port,
});

export const asRemoveCustomdForm = (csrfToken: string, sid: number) => ({
  csrfToken,
  sid: `${sid}`,
});

export const asCustomdForm = (csrfToken: string, service: Service) => ({
  csrfToken,
  sid: `${service.sid}`,
  name: service.name,
  uri: service.uri,
  secureUri: service.secureUri,
  pollInterval: `${service.pollInterval || 1200}`,
  authType: service.authType,
  stringsVersion: service.strings?.version || "0",
  stringsUri: service.strings?.uri || "",
  presentationMapVersion: service.presentation?.version || "0",
  presentationMapUri: service.presentation?.uri || "",
  containerType: "MService",
  caps: BONOB_CAPABILITIES,
});

const setupDiscovery = (
  manager: SonosManager,
  sonosSeedHost?: string
): Promise<boolean> => {
  if (sonosSeedHost == undefined || sonosSeedHost == "") {
    logger.info("Trying to discover sonos devices");
    return manager.InitializeWithDiscovery(10);
  } else {
    logger.info(`Trying to discover sonos devices using seed ${sonosSeedHost}`);
    return manager.InitializeFromDevice(sonosSeedHost);
  }
};

export function autoDiscoverySonos(sonosSeedHost?: string): Sonos {
  const sonosDevices = async (): Promise<SonosDevice[]> => {
    const manager = new SonosManager();
    return setupDiscovery(manager, sonosSeedHost)
      .then((success) => {
        if (success) {
          return manager.Devices;
        } else {
          logger.warn("Didn't find any sonos devices!");
          return [];
        }
      })
      .catch((e) => {
        logger.error(`Failed looking for sonos devices`, { cause: e });
        return [];
      });
  };

  const post = async (
    action: string,
    customdForm: (csrfToken: string) => any
  ) => {
    const anyDevice = await sonosDevices().then((devices) => head(devices));

    if (!anyDevice) {
      logger.warn("Failed to find a device to register with...");
      return false;
    }

    logger.info(
      `${action} using sonos device ${anyDevice.Name} @ ${anyDevice.Host}`
    );

    const customd = `http://${anyDevice.Host}:${anyDevice.Port}/customsd`;

    const csrfToken = await axios.get(customd).then((response) =>
      parse(response.data)
        .querySelectorAll("input")
        .find((it) => it.getAttribute("name") == "csrfToken")
        ?.getAttribute("value")
    );

    if (!csrfToken) {
      logger.warn(
        `Failed to find csrfToken at GET -> ${customd}, cannot ${action} service`
      );
      return false;
    }
    const form = customdForm(csrfToken);
    logger.info(`${action} with sonos @ ${customd}`, { form });
    return axios
      .post(customd, new URLSearchParams(qs.stringify(form)), {
        headers: {
          "Content-Type": "application/x-www-form-urlencoded",
        },
      })
      .then((response) => response.status == 200);
  };

  return {
    devices: async () => sonosDevices().then((it) => it.map(asDevice)),

    services: async () =>
      sonosDevices()
        .then((it) => head(it))
        .then(
          (device) =>
            device?.MusicServicesService?.ListAndParseAvailableServices() || []
        )
        .then((it) => it.map(asService)),

    remove: async (sid: number) =>
      post("remove", (csrfToken) => asRemoveCustomdForm(csrfToken, sid)),

    register: async (service: Service) =>
      post("register", (csrfToken) => asCustomdForm(csrfToken, service)),
  };
}

export type Discovery = {
  enabled: boolean;
  seedHost?: string;
};

export default (sonosDiscovery: Discovery = { enabled: true }): Sonos =>
  sonosDiscovery.enabled
    ? autoDiscoverySonos(sonosDiscovery.seedHost)
    : SONOS_DISABLED;
