/*
This file is part of the Notesnook project (https://notesnook.com/)

Copyright (C) 2023 Streetwriters (Private) Limited

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

import Sodium from "@ammarahmed/react-native-sodium";
import {
  FileEncryptionMetadataWithHash,
  FileEncryptionMetadataWithOutputType,
  Output,
  RequestOptions
} from "@notesnook/core";
import { DataFormat, SerializedKey } from "@notesnook/crypto";
import { Platform } from "react-native";
import RNFetchBlob from "react-native-blob-util";
import { eSendEvent } from "../../services/event-manager";
import { IOS_APPGROUPID } from "../../utils/constants";
import { DatabaseLogger, db } from "../database";
import {
  ABYTES,
  cacheDir,
  cacheDirOld,
  getRandomId,
  isSuccessStatusCode,
  parseS3Error
} from "./utils";

export async function readEncrypted<TOutputFormat extends DataFormat>(
  filename: string,
  key: SerializedKey,
  cipherData: FileEncryptionMetadataWithOutputType<TOutputFormat>
) {
  await migrateFilesFromCache();
  DatabaseLogger.log("Read encrypted file...");
  const path = `${cacheDir}/${filename}`;

  try {
    if (!(await exists(filename))) {
      return;
    }

    const output = await Sodium.decryptFile(
      key,
      {
        ...cipherData,
        hash: filename,
        appGroupId: IOS_APPGROUPID
      },
      cipherData.outputType === "base64" ? "base64" : "text"
    );

    DatabaseLogger.log("File decrypted...");

    return output as Output<TOutputFormat>;
  } catch (e) {
    RNFetchBlob.fs.unlink(path).catch(() => {
      /* empty */
    });
    DatabaseLogger.error(e);
  }
}

export async function hashBase64(data: string) {
  const hash = await Sodium.hashFile({
    type: "base64",
    data,
    uri: ""
  });
  return {
    hash: hash,
    type: "xxh64"
  };
}

export async function writeEncryptedBase64(
  data: string,
  encryptionKey: SerializedKey,
  mimeType: string
): Promise<FileEncryptionMetadataWithHash> {
  await createCacheDir();
  const filepath = cacheDir + `/${getRandomId("imagecache_")}`;
  await RNFetchBlob.fs.writeFile(filepath, data, "base64");
  const output = await Sodium.encryptFile(encryptionKey, {
    uri: Platform.OS === "ios" ? filepath : "file://" + filepath,
    type: "url"
  });

  RNFetchBlob.fs.unlink(filepath).catch(() => {
    /* empty */
  });

  return {
    ...output,
    alg: "xcha-stream"
  };
}

async function deleteLocalFile(filename: string) {
  try {
    await createCacheDir();
    const path = cacheDir + `/${filename}`;
    const exists = await RNFetchBlob.fs.exists(path);
    if (Platform.OS === "ios" && !exists) {
      const iosAppGroup =
        Platform.OS === "ios"
          ? await (RNFetchBlob.fs as any).pathForAppGroup(IOS_APPGROUPID)
          : null;
      const appGroupPath = `${iosAppGroup}/${filename}`;
      if (await RNFetchBlob.fs.exists(appGroupPath)) {
        RNFetchBlob.fs.unlink(appGroupPath).catch(() => {
          /* empty */
        });
        return;
      }
    }
    if (exists) {
      RNFetchBlob.fs.unlink(path).catch(() => {
        /* empty */
      });
    }
  } catch (e) {
    DatabaseLogger.error(e as Error, "deleteLocalFile");
  }
}

export async function deleteFile(
  filename: string,
  requestOptions?: RequestOptions
): Promise<boolean> {
  await createCacheDir();
  if (!requestOptions) {
    deleteLocalFile(filename);
    return true;
  }

  const { url, headers } = requestOptions;

  try {
    const response = await RNFetchBlob.fetch("DELETE", url, headers);
    const status = response.info().status;
    const ok = status >= 200 && status < 300;
    if (ok) {
      deleteLocalFile(filename);
    }
    return ok;
  } catch (e) {
    DatabaseLogger.error(e, "Delete file", {
      url: url
    });
    return false;
  }
}

export async function bulkDeleteFiles(
  filenames: string[],
  requestOptions?: RequestOptions
) {
  await createCacheDir();
  if (!requestOptions) {
    filenames.forEach((filename) => {
      deleteLocalFile(filename);
    });
    return true;
  }

  try {
    const { url, headers } = requestOptions;
    const response = await fetch(url, {
      method: "POST",
      headers: {
        ...headers,
        "Content-Type": "application/json"
      },
      body: JSON.stringify({
        names: filenames
      })
    });

    const result = isSuccessStatusCode(response.status);
    if (result) {
      filenames.forEach((filename) => {
        deleteLocalFile(filename);
      });
    } else {
      throw await response.text();
    }
    return result;
  } catch (e) {
    DatabaseLogger.error(
      typeof e === "string" ? parseS3Error(e as string) : (e as Error),
      "Could not bulk delete files"
    );
    return false;
  }
}

export async function clearFileStorage() {
  try {
    await createCacheDir();
    const files = await RNFetchBlob.fs.ls(cacheDir);
    const oldCache = await RNFetchBlob.fs.ls(cacheDirOld);

    for (const file of files) {
      await RNFetchBlob.fs.unlink(cacheDir + `/${file}`).catch(() => {
        /* empty */
      });
    }
    for (const file of oldCache) {
      await RNFetchBlob.fs.unlink(cacheDirOld + `/${file}`).catch(() => {
        /* empty */
      });
    }
  } catch (e) {
    DatabaseLogger.error(e, "clearFileStorage");
  }
}

export async function createCacheDir() {
  try {
    if (!(await RNFetchBlob.fs.exists(cacheDir))) {
      await RNFetchBlob.fs.mkdir(cacheDir);
      DatabaseLogger.log("Cache directory created");
    }
  } catch (e) {
    DatabaseLogger.error(e);
  }
}

export async function migrateFilesFromCache() {
  try {
    await createCacheDir();
    const migratedFilesPath = cacheDir + "/.migrated_1";
    const migrated = await RNFetchBlob.fs.exists(migratedFilesPath);
    if (migrated) {
      return;
    }

    const files = await RNFetchBlob.fs.ls(cacheDir);

    const oldCache = await RNFetchBlob.fs.ls(cacheDirOld);
    for (const file of oldCache) {
      if (file.startsWith("org.") || file.startsWith("com.")) continue;
      RNFetchBlob.fs
        .mv(cacheDirOld + `/${file}`, cacheDir + `/${file}`)
        .catch(() => {
          /* empty */
        });
    }
    await RNFetchBlob.fs.createFile(migratedFilesPath, "1", "utf8");
  } catch (e) {
    DatabaseLogger.error(e, "migrateFilesFromCache");
  }
}

export async function clearCache() {
  await RNFetchBlob.fs.unlink(cacheDir).catch(() => {
    /* empty */
  });
  await createCacheDir();
  eSendEvent("cache-cleared");
}

export async function deleteCacheFileByPath(path: string) {
  await RNFetchBlob.fs.unlink(path).catch(() => {
    /* empty */
  });
}

export async function deleteCacheFileByName(name: string) {
  const iosAppGroup =
    Platform.OS === "ios"
      ? await (RNFetchBlob.fs as any).pathForAppGroup(IOS_APPGROUPID)
      : null;
  const appGroupPath = `${iosAppGroup}/${name}`;
  await RNFetchBlob.fs.unlink(appGroupPath).catch(() => {
    /* empty */
  });
  await RNFetchBlob.fs.unlink(`${cacheDir}/${name}`).catch(() => {
    /* empty */
  });
}

export async function deleteDCacheFiles() {
  try {
    await createCacheDir();
    const files = await RNFetchBlob.fs.ls(cacheDir);
    for (const file of files) {
      if (
        file.includes("_dcache") ||
        file.startsWith("NN_") ||
        file.endsWith(".pdf")
      ) {
        await RNFetchBlob.fs.unlink(file).catch(() => {
          /* empty */
        });
      }
    }
  } catch (e) {
    /** Empty */
  }
}

export async function getCachePathForFile(filename: string) {
  const path = `${cacheDir}/${filename}`;

  const iosAppGroup =
    Platform.OS === "ios"
      ? await (RNFetchBlob.fs as any).pathForAppGroup(IOS_APPGROUPID)
      : null;
  const appGroupPath = `${iosAppGroup}/${filename}`;

  const exists = await RNFetchBlob.fs.exists(path);

  // Check if file is present in app group path.
  let existsInAppGroup = false;
  if (!exists && Platform.OS === "ios") {
    existsInAppGroup = await RNFetchBlob.fs.exists(appGroupPath);
  }

  return existsInAppGroup ? appGroupPath : path;
}

export async function exists(filename: string) {
  const path = `${cacheDir}/${filename}`;

  const iosAppGroup =
    Platform.OS === "ios"
      ? await (RNFetchBlob.fs as any).pathForAppGroup(IOS_APPGROUPID)
      : null;
  const appGroupPath = `${iosAppGroup}/${filename}`;

  let exists = await RNFetchBlob.fs.exists(path);

  // Check if file is present in app group path.
  let existsInAppGroup = false;
  if (!exists && Platform.OS === "ios") {
    existsInAppGroup = await RNFetchBlob.fs.exists(appGroupPath);
  }

  if (exists || existsInAppGroup) {
    const attachment = await db.attachments.attachment(filename);
    if (!attachment) return false;
    const totalChunks = Math.ceil(attachment.size / attachment.chunkSize);
    const totalAbytes = totalChunks * ABYTES;
    const expectedFileSize = attachment.size + totalAbytes;

    const stat = await RNFetchBlob.fs.stat(
      existsInAppGroup ? appGroupPath : path
    );

    if (stat.size !== expectedFileSize) {
      DatabaseLogger.log(
        `File size mismatch: ${filename}, expected: ${expectedFileSize}, actual: ${stat.size}`
      );
      RNFetchBlob.fs
        .unlink(existsInAppGroup ? appGroupPath : path)
        .catch(() => {
          /* empty */
        });
      return false;
    }

    exists = true;
  }
  return exists;
}

export async function bulkExists(files: string[]) {
  try {
    await createCacheDir();
    const cacheFiles = await RNFetchBlob.fs.ls(cacheDir);
    let missingFiles = files.filter((file) => !cacheFiles.includes(file));

    if (Platform.OS === "ios") {
      const iosAppGroup =
        Platform.OS === "ios"
          ? await (RNFetchBlob.fs as any).pathForAppGroup(IOS_APPGROUPID)
          : null;
      const appGroupFiles = await RNFetchBlob.fs.ls(iosAppGroup);
      missingFiles = missingFiles.filter(
        (file) => !appGroupFiles.includes(file)
      );
    }
    return missingFiles;
  } catch (e) {
    DatabaseLogger.error(e);
    return [];
  }
}

export async function getCacheSize() {
  await createCacheDir();
  const stat = await RNFetchBlob.fs.lstat(`file://` + cacheDir);
  let total = 0;

  stat.forEach((file) => {
    total += parseInt(file.size as unknown as string);
  });
  return total;
}
