import { prisma, unifi, unifiUsername, unifiPassword } from "../constants";
import GenericError from "../Errors/GenericError";
import { UnifiSite } from "../../generated/prisma/client";
import { events } from "../modules/globalEvents";

let loggedIn = false;

async function ensureLoggedIn(): Promise<void> {
  if (loggedIn) return;
  if (!unifiPassword)
    throw new GenericError({
      name: "UnifiConfigError",
      message: "UniFi controller credentials are not configured.",
      status: 503,
    });
  await unifi.login(unifiUsername, unifiPassword);
  loggedIn = true;
}

/**
 * Wraps a UniFi API call with automatic re-authentication on failure.
 * If the call fails and we were previously logged in (i.e. session likely
 * expired), resets the login state, re-authenticates, and retries once.
 */
async function withReauth<T>(fn: () => Promise<T>): Promise<T> {
  await ensureLoggedIn();
  try {
    return await fn();
  } catch (e) {
    if (!loggedIn) throw e;
    events.emit("unifi:reauth");
    loggedIn = false;
    await ensureLoggedIn();
    return fn();
  }
}

export const unifiSites = {
  /**
   * Fetch a UniFi site record from the database by its internal ID.
   */
  async fetch(id: string): Promise<UnifiSite> {
    const site = await prisma.unifiSite.findFirst({
      where: { id },
    });

    if (!site)
      throw new GenericError({
        name: "UnifiSiteNotFound",
        message: `UniFi site with id '${id}' was not found.`,
        status: 404,
      });

    return site;
  },

  /**
   * Fetch all UniFi site records from the database.
   */
  async fetchAll(): Promise<UnifiSite[]> {
    return prisma.unifiSite.findMany({
      include: { company: true },
    });
  },

  /**
   * Fetch all UniFi site records linked to a specific company.
   */
  async fetchByCompany(companyId: number): Promise<UnifiSite[]> {
    return prisma.unifiSite.findMany({
      where: { companyId },
    });
  },

  /**
   * Link a UniFi site to a company.
   */
  async linkToCompany(siteId: string, companyId: number): Promise<UnifiSite> {
    const site = await prisma.unifiSite.findFirst({ where: { id: siteId } });
    if (!site)
      throw new GenericError({
        name: "UnifiSiteNotFound",
        message: `UniFi site '${siteId}' was not found.`,
        status: 404,
      });

    const company = await prisma.company.findFirst({
      where: { id: companyId },
    });
    if (!company)
      throw new GenericError({
        name: "CompanyNotFound",
        message: `Company '${companyId}' was not found.`,
        status: 404,
      });

    return prisma.unifiSite.update({
      where: { id: siteId },
      data: { companyId },
    });
  },

  /**
   * Unlink a UniFi site from its company.
   */
  async unlinkFromCompany(siteId: string): Promise<UnifiSite> {
    return prisma.unifiSite.update({
      where: { id: siteId },
      data: { companyId: null },
    });
  },

  /**
   * Sync all sites from the UniFi controller into the database.
   * Creates new records for sites not yet tracked, updates names for existing ones.
   */
  async syncSites(): Promise<UnifiSite[]> {
    events.emit("unifi:sites:sync:started");

    // Fetch all sites from the controller
    const allSites = await withReauth(() => unifi.getAllSites());

    const results: UnifiSite[] = [];
    let created = 0;
    let updated = 0;

    for (const site of allSites) {
      const existing = await prisma.unifiSite.findFirst({
        where: { siteId: site.name },
      });

      if (existing) {
        const updatedSite = await prisma.unifiSite.update({
          where: { id: existing.id },
          data: { name: site.description },
        });
        results.push(updatedSite);
        updated++;
      } else {
        const createdSite = await prisma.unifiSite.create({
          data: {
            name: site.description,
            siteId: site.name,
          },
        });
        results.push(createdSite);
        created++;
      }
    }

    events.emit("unifi:sites:sync:completed", {
      total: results.length,
      created,
      updated,
    });

    return results;
  },

  /**
   * Get live site overview from the UniFi controller.
   */
  async getSiteOverview(siteId: string) {
    return withReauth(() => unifi.getSiteOverview(siteId));
  },

  /**
   * Get live devices from the UniFi controller for a site.
   */
  async getDevices(siteId: string) {
    return withReauth(() => unifi.getDevices(siteId));
  },

  /**
   * Get live WiFi networks (WLANs) from the UniFi controller for a site.
   */
  async getWlanConf(siteId: string) {
    return withReauth(() => unifi.getWlanConf(siteId));
  },

  /**
   * Update a WiFi network on the UniFi controller.
   */
  async updateWlanConf(
    siteId: string,
    wlanId: string,
    updates: Parameters<typeof unifi.updateWlanConf>[2],
  ) {
    return withReauth(() => unifi.updateWlanConf(siteId, wlanId, updates));
  },

  /**
   * Get live network configurations from the UniFi controller for a site.
   */
  async getNetworks(siteId: string) {
    return withReauth(() => unifi.getNetworks(siteId));
  },

  /**
   * Create a new site on the UniFi controller and track it in the database.
   */
  async createSite(description: string): Promise<UnifiSite> {
    const created = await withReauth(() => unifi.createSite(description));

    return prisma.unifiSite.create({
      data: {
        name: created.description,
        siteId: created.name,
      },
    });
  },

  /**
   * Get WLAN groups from the UniFi controller for a site.
   */
  async getWlanGroups(siteId: string) {
    return withReauth(() => unifi.getWlanGroups(siteId));
  },

  /**
   * Create a new WLAN group (AP broadcasting group) on the UniFi controller.
   */
  async createWlanGroup(
    siteId: string,
    input: Parameters<typeof unifi.createWlanGroup>[1],
  ) {
    return withReauth(() => unifi.createWlanGroup(siteId, input));
  },

  /**
   * Get user groups (speed profiles) from the UniFi controller for a site.
   */
  async getUserGroups(siteId: string) {
    return withReauth(() => unifi.getUserGroups(siteId));
  },

  /**
   * Create a new user group (speed profile) on the UniFi controller.
   */
  async createUserGroup(
    siteId: string,
    input: Parameters<typeof unifi.createUserGroup>[1],
  ) {
    return withReauth(() => unifi.createUserGroup(siteId, input));
  },

  /**
   * Get AP groups from the UniFi controller for a site.
   */
  async getApGroups(siteId: string) {
    return withReauth(() => unifi.getApGroups(siteId));
  },

  /**
   * Create a new AP group on the UniFi controller.
   */
  async createApGroup(
    siteId: string,
    name: string,
    deviceMacs: string[],
    forWlanconf: boolean = false,
  ) {
    return withReauth(() =>
      unifi.createApGroup(siteId, name, deviceMacs, forWlanconf),
    );
  },

  /**
   * Update an existing AP group's device MACs on the UniFi controller.
   */
  async updateApGroup(siteId: string, groupId: string, deviceMacs: string[]) {
    return withReauth(() => unifi.updateApGroup(siteId, groupId, deviceMacs));
  },

  /**
   * Get access points only from the UniFi controller for a site.
   */
  async getAccessPoints(siteId: string) {
    return withReauth(() => unifi.getAccessPoints(siteId));
  },

  /**
   * Get WiFi SSID limits per AP per radio band.
   */
  async getWifiLimits(siteId: string) {
    return withReauth(() => unifi.getWifiLimits(siteId));
  },

  /**
   * Get private pre-shared keys for a specific WLAN.
   */
  async getPrivatePSKs(siteId: string, wlanId: string) {
    return withReauth(() => unifi.getPrivatePSKs(siteId, wlanId));
  },

  /**
   * Create a private pre-shared key on a specific WLAN.
   */
  async createPrivatePSK(
    siteId: string,
    wlanId: string,
    psk: Parameters<typeof unifi.createPrivatePSK>[2],
  ) {
    return withReauth(() => unifi.createPrivatePSK(siteId, wlanId, psk));
  },
};
