import { setHours, setMinutes } from "date-fns";
import type { HassConfig } from "home-assistant-js-websocket";
import memoizeOne from "memoize-one";
import checkValidDate from "../common/datetime/check_valid_date";
import {
  formatDateTime,
  formatDateTimeNumeric,
} from "../common/datetime/format_date_time";
import { formatTime } from "../common/datetime/format_time";
import type { LocalizeFunc } from "../common/translations/localize";
import type { HomeAssistant } from "../types";
import { documentationUrl } from "../util/documentation-url";
import { fileDownload } from "../util/file_download";
import { handleFetchPromise } from "../util/hass-call-api";
import type { BackupManagerState, ManagerStateEvent } from "./backup_manager";
import { domainToName } from "./integration";
import type { FrontendLocaleData } from "./translation";

export const enum BackupScheduleRecurrence {
  NEVER = "never",
  DAILY = "daily",
  CUSTOM_DAYS = "custom_days",
}

export type BackupDay = "mon" | "tue" | "wed" | "thu" | "fri" | "sat" | "sun";

export const BACKUP_DAYS: BackupDay[] = [
  "mon",
  "tue",
  "wed",
  "thu",
  "fri",
  "sat",
  "sun",
];

export const sortWeekdays = (weekdays) =>
  weekdays.sort((a, b) => BACKUP_DAYS.indexOf(a) - BACKUP_DAYS.indexOf(b));

export interface Retention {
  copies?: number | null;
  days?: number | null;
}

export interface BackupConfig {
  automatic_backups_configured: boolean;
  last_attempted_automatic_backup: string | null;
  last_completed_automatic_backup: string | null;
  next_automatic_backup: string | null;
  next_automatic_backup_additional?: boolean;
  create_backup: {
    agent_ids: string[];
    include_addons: string[] | null;
    include_all_addons: boolean;
    include_database: boolean;
    include_folders: string[] | null;
    name: string | null;
    password: string | null;
  };
  retention: Retention;
  schedule: {
    recurrence: BackupScheduleRecurrence;
    time?: string | null;
    days: BackupDay[];
  };
  agents: BackupAgentsConfig;
}

export interface BackupMutableConfig {
  automatic_backups_configured?: boolean;
  create_backup?: {
    agent_ids?: string[];
    include_addons?: string[];
    include_all_addons?: boolean;
    include_database?: boolean;
    include_folders?: string[];
    name?: string | null;
    password?: string | null;
  };
  retention?: Retention;
  schedule?: {
    recurrence: BackupScheduleRecurrence;
    time?: string | null;
    days?: BackupDay[] | null;
  };
  agents?: BackupAgentsConfig;
}

export type BackupAgentsConfig = Record<string, BackupAgentConfig>;

export interface BackupAgentConfig {
  protected?: boolean;
  retention?: Retention | null;
}

export interface BackupAgent {
  agent_id: string;
  name: string;
}

export interface BackupContentAgent {
  size: number;
  protected: boolean;
}

export interface AddonInfo {
  name: string | null;
  slug: string;
  version: string | null;
}

export interface BackupContent {
  backup_id: string;
  date: string;
  name: string;
  agents: Record<string, BackupContentAgent>;
  failed_agent_ids?: string[];
  failed_addons?: AddonInfo[];
  failed_folders?: string[];
  extra_metadata?: {
    "supervisor.addon_update"?: string;
    "supervisor.app_update"?: string;
  };
  with_automatic_settings: boolean;
}

export interface BackupData {
  addons: BackupAddon[];
  database_included: boolean;
  folders: string[];
  homeassistant_version: string;
  homeassistant_included: boolean;
}

export interface BackupAddon {
  name: string;
  slug: string;
  version: string;
}

export interface BackupContentExtended extends BackupContent, BackupData {}

export interface BackupInfo {
  backups: BackupContent[];
  agent_errors: Record<string, string>;
  last_attempted_automatic_backup: string | null;
  last_completed_automatic_backup: string | null;
  last_action_event: ManagerStateEvent | null;
  next_automatic_backup: string | null;
  next_automatic_backup_additional: boolean;
  state: BackupManagerState;
}

export interface BackupDetails {
  backup: BackupContentExtended;
}

export interface BackupAgentsInfo {
  agents: BackupAgent[];
}

export interface GenerateBackupParams {
  agent_ids: string[];
  include_addons?: string[];
  include_all_addons?: boolean;
  include_database?: boolean;
  include_folders?: string[];
  include_homeassistant?: boolean;
  name?: string;
  password?: string;
}

export interface RestoreBackupParams {
  backup_id: string;
  agent_id: string;
  password?: string;
  restore_addons?: string[];
  restore_database?: boolean;
  restore_folders?: string[];
  restore_homeassistant?: boolean;
}

export type CloudBackupHealth = "success" | "failed" | "old" | "none";

export const fetchBackupConfig = (hass: Pick<HomeAssistant, "callWS">) =>
  hass.callWS<{ config: BackupConfig }>({ type: "backup/config/info" });

export const updateBackupConfig = (
  hass: HomeAssistant,
  config: BackupMutableConfig
) => hass.callWS({ type: "backup/config/update", ...config });

export const saveBackupConfig = (hass: HomeAssistant, config: BackupConfig) =>
  updateBackupConfig(hass, {
    create_backup: {
      agent_ids: config.create_backup.agent_ids,
      include_folders: config.create_backup.include_folders ?? [],
      include_database: config.create_backup.include_database,
      include_addons: config.create_backup.include_addons ?? [],
      include_all_addons: config.create_backup.include_all_addons,
      password: config.create_backup.password,
    },
    retention: config.retention,
    schedule: config.schedule,
  });

export const getBackupDownloadUrl = (
  id: string,
  agentId: string,
  password?: string | null
) =>
  `/api/backup/download/${id}?agent_id=${agentId}${password ? `&password=${password}` : ""}`;

export const fetchBackupInfo = (hass: HomeAssistant): Promise<BackupInfo> =>
  hass.callWS({
    type: "backup/info",
  });

export const fetchBackupDetails = (
  hass: HomeAssistant,
  id: string
): Promise<BackupDetails> =>
  hass.callWS({
    type: "backup/details",
    backup_id: id,
  });

export const fetchBackupAgentsInfo = (
  hass: HomeAssistant
): Promise<BackupAgentsInfo> =>
  hass.callWS({
    type: "backup/agents/info",
  });

export const deleteBackup = (hass: HomeAssistant, id: string): Promise<void> =>
  hass.callWS({
    type: "backup/delete",
    backup_id: id,
  });

export const generateBackup = (
  hass: HomeAssistant,
  params: GenerateBackupParams
): Promise<{ backup_id: string }> =>
  hass.callWS({
    type: "backup/generate",
    ...params,
  });

export const generateBackupWithAutomaticSettings = (
  hass: HomeAssistant
): Promise<void> =>
  hass.callWS({
    type: "backup/generate_with_automatic_settings",
  });

export const restoreBackup = (
  hass: HomeAssistant,
  params: RestoreBackupParams
): Promise<{ backup_id: string }> =>
  hass.callWS({
    type: "backup/restore",
    ...params,
  });

export const uploadBackup = async (
  hass: HomeAssistant,
  file: File,
  agentIds: string[]
): Promise<{ backup_id: string }> => {
  const fd = new FormData();
  fd.append("file", file);

  const params = new URLSearchParams();

  agentIds.forEach((agentId) => {
    params.append("agent_id", agentId);
  });

  return handleFetchPromise(
    hass.fetchWithAuth(`/api/backup/upload?${params.toString()}`, {
      method: "POST",
      body: fd,
    })
  );
};

export const getPreferredAgentForDownload = (agents: string[]) => {
  const localAgent = agents.find(isLocalAgent);
  if (localAgent) {
    return localAgent;
  }
  const networkMountAgent = agents.find(isNetworkMountAgent);
  if (networkMountAgent) {
    return networkMountAgent;
  }

  return agents[0];
};

export const canDecryptBackupOnDownload = (
  hass: HomeAssistant,
  backup_id: string,
  agent_id: string,
  password: string
) =>
  hass.callWS({
    type: "backup/can_decrypt_on_download",
    backup_id,
    agent_id,
    password,
  });

export const CORE_LOCAL_AGENT = "backup.local";
export const HASSIO_LOCAL_AGENT = "hassio.local";
export const CLOUD_AGENT = "cloud.cloud";

// How many hours a scheduled automatic backup may be behind before it reads as
// overdue, so a few hours of scheduler lag (or a daylight saving shift) doesn't
// show a warning.
export const BACKUP_OVERDUE_MARGIN_HOURS = 3;

export const isLocalAgent = (agentId: string) =>
  [CORE_LOCAL_AGENT, HASSIO_LOCAL_AGENT].includes(agentId);

export const getLastCloudBackup = (
  backups?: BackupContent[]
): BackupContent | undefined =>
  backups
    ?.filter((backup) => CLOUD_AGENT in backup.agents)
    .sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime())[0];

export const cloudBackupEnabled = (backupConfig?: BackupConfig): boolean =>
  !!backupConfig?.automatic_backups_configured &&
  backupConfig.create_backup.agent_ids.includes(CLOUD_AGENT);

const BACKUP_OVERDUE_MARGIN_MS = BACKUP_OVERDUE_MARGIN_HOURS * 60 * 60 * 1000;

export const cloudBackupHealth = (
  backupConfig?: BackupConfig
): CloudBackupHealth => {
  if (!cloudBackupEnabled(backupConfig)) {
    return "none";
  }

  const completed = backupConfig?.last_completed_automatic_backup
    ? new Date(backupConfig.last_completed_automatic_backup).getTime()
    : 0;

  const attempted = backupConfig?.last_attempted_automatic_backup
    ? new Date(backupConfig.last_attempted_automatic_backup).getTime()
    : 0;

  if (!completed && !attempted) {
    return "none";
  }

  if (attempted > completed) {
    return "failed";
  }

  const next =
    backupConfig?.next_automatic_backup &&
    new Date(backupConfig.next_automatic_backup).getTime();

  if (next && next < Date.now() - BACKUP_OVERDUE_MARGIN_MS) {
    return "old";
  }

  return "success";
};

export const isNetworkMountAgent = (agentId: string) => {
  const [domain, name] = agentId.split(".");
  return domain === "hassio" && name !== "local";
};

export const computeBackupAgentName = (
  localize: LocalizeFunc,
  agentId: string,
  agents: BackupAgent[]
) => {
  if (isLocalAgent(agentId)) {
    return localize("ui.panel.config.backup.agents.local_agent");
  }

  const agent = agents.find((a) => a.agent_id === agentId);

  const domain = agentId.split(".")[0];
  const name = agent ? agent.name : agentId.split(".")[1];

  // If it's a network mount agent, only show the name
  if (isNetworkMountAgent(agentId)) {
    return name;
  }

  const domainName = domainToName(localize, domain);

  // If there are multiple agents for a domain, show the name
  const showName =
    agents.filter((a) => a.agent_id.split(".")[0] === domain).length > 1;

  return showName ? `${domainName}: ${name}` : domainName;
};

export const computeBackupSize = (backup: BackupContent) =>
  Math.max(...Object.values(backup.agents).map((agent) => agent.size));

export type BackupType = "automatic" | "manual" | "app_update";

const BACKUP_TYPE_ORDER: BackupType[] = ["automatic", "app_update", "manual"];

export const getBackupTypes = memoizeOne((isHassio: boolean) =>
  isHassio
    ? BACKUP_TYPE_ORDER
    : BACKUP_TYPE_ORDER.filter((type) => type !== "app_update")
);

export const computeBackupType = (
  backup: BackupContent,
  isHassio: boolean
): BackupType => {
  if (backup.with_automatic_settings) {
    return "automatic";
  }
  if (
    isHassio &&
    (backup.extra_metadata?.["supervisor.addon_update"] != null ||
      backup.extra_metadata?.["supervisor.app_update"] != null)
  ) {
    return "app_update";
  }
  return "manual";
};

export const compareAgents = (a: string, b: string) => {
  const isLocalA = isLocalAgent(a);
  const isLocalB = isLocalAgent(b);
  const isNetworkMountAgentA = isNetworkMountAgent(a);
  const isNetworkMountAgentB = isNetworkMountAgent(b);

  const getPriority = (isLocal: boolean, isNetworkMount: boolean) => {
    if (isLocal) return 1;
    if (isNetworkMount) return 2;
    return 3;
  };

  const priorityA = getPriority(isLocalA, isNetworkMountAgentA);
  const priorityB = getPriority(isLocalB, isNetworkMountAgentB);

  if (priorityA !== priorityB) {
    return priorityA - priorityB;
  }

  return a.localeCompare(b);
};

export const generateEncryptionKey = () => {
  const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
  const pattern = "xxxx-xxxx-xxxx-xxxx-xxxx-xxxx-xxxx";
  let result = "";
  const randomArray = new Uint8Array(pattern.length);
  crypto.getRandomValues(randomArray);
  randomArray.forEach((number, index) => {
    result += pattern[index] === "-" ? "-" : chars[number % chars.length];
  });
  return result;
};

export const generateEmergencyKit = (
  hass: HomeAssistant,
  encryptionKey: string
) =>
  "data:text/plain;charset=utf-8," +
  encodeURIComponent(`${hass.localize("ui.panel.config.backup.emergency_kit_file.title")}

${hass.localize("ui.panel.config.backup.emergency_kit_file.description")}

${hass.localize("ui.panel.config.backup.emergency_kit_file.date")} ${formatDateTime(new Date(), hass.locale, hass.config)}

${hass.localize("ui.panel.config.backup.emergency_kit_file.instance")}
${hass.config.location_name}

${hass.localize("ui.panel.config.backup.emergency_kit_file.url")}
${hass.auth.data.hassUrl}

${hass.localize("ui.panel.config.backup.emergency_kit_file.encryption_key")}
${encryptionKey}

${hass.localize("ui.panel.config.backup.emergency_kit_file.more_info", { link: documentationUrl(hass, "/more-info/backup-emergency-kit") })}`);

export const geneateEmergencyKitFileName = (
  hass: HomeAssistant,
  append?: string
) =>
  `home_assistant_backup_emergency_kit_${append ? `${append}_` : ""}${formatDateTimeNumeric(new Date(), hass.locale, hass.config).replace(",", "").replace(" ", "_")}.txt`;

export const downloadEmergencyKit = (
  hass: HomeAssistant,
  key: string,
  appendFileName?: string
) =>
  fileDownload(
    generateEmergencyKit(hass, key),
    geneateEmergencyKitFileName(hass, appendFileName)
  );

export const DEFAULT_OPTIMIZED_BACKUP_START_TIME = setMinutes(
  setHours(new Date(), 4),
  45
);

export const DEFAULT_OPTIMIZED_BACKUP_END_TIME = setMinutes(
  setHours(new Date(), 5),
  45
);

export const getFormattedBackupTime = memoizeOne(
  (
    locale: FrontendLocaleData,
    config: HassConfig,
    backupTime?: Date | string | null
  ) => {
    if (checkValidDate(backupTime as Date)) {
      return formatTime(backupTime as Date, locale, config);
    }
    if (typeof backupTime === "string" && backupTime) {
      const splitted = backupTime.split(":");
      const date = setMinutes(
        setHours(new Date(), parseInt(splitted[0])),
        parseInt(splitted[1])
      );
      return formatTime(date, locale, config);
    }

    return `${formatTime(DEFAULT_OPTIMIZED_BACKUP_START_TIME, locale, config)} - ${formatTime(DEFAULT_OPTIMIZED_BACKUP_END_TIME, locale, config)}`;
  }
);

export const SUPPORTED_UPLOAD_FORMAT = "application/x-tar";

// Browsers report the MIME type of a .tar inconsistently (Firefox on Windows
// gives an empty or different type), so accept it by extension as well.
export const isSupportedBackupFile = (file: File): boolean =>
  file.type === SUPPORTED_UPLOAD_FORMAT ||
  file.name.toLowerCase().endsWith(".tar");

export interface BackupUploadFileFormData {
  file?: File;
}

export const INITIAL_UPLOAD_FORM_DATA: BackupUploadFileFormData = {
  file: undefined,
};
