/* eslint-disable arrow-parens */
/* eslint-disable unicorn/no-array-for-each */
import { execSync } from "child_process"
import { flags } from "@oclif/command"
import SessionCommand from "../utils/SessionCommand"
import SessionManager from "../managers/session"
import * as fs from "fs"
import * as path from "path"
import * as archiver from "archiver"
import axios from "axios"
import FormData = require("form-data");
import Console from "../utils/console"
import {
  decompress,
  downloadEditor,
  checkIfDirectoryExists,
} from "../managers/file"
import api, { getConsumable, RIGOBOT_HOST, TAcademy } from "../utils/api"
import * as prompts from "prompts"
import { isValidRigoToken } from "../utils/rigoActions"
import { minutesToISO8601Duration } from "../utils/misc"
import { slugify } from "../utils/creatorUtilities"

const uploadZipEndpont = RIGOBOT_HOST + "/v1/learnpack/upload"

export const handleAssetCreation = async (
  sessionPayload: { token: string; rigobotToken: string },
  learnJson: any,
  selectedLang: string,
  learnpackDeployUrl: string,
  b64IndexReadme: string,
  all_translations: string[] = []
) => {
  const category = "uncategorized"

  try {
    const user = await api.validateToken(sessionPayload.token)

    let slug = slugify(learnJson.title[selectedLang]).slice(0, 47)
    slug = `${slug}-${selectedLang}`

    const { exists } = await api.doesAssetExists(sessionPayload.token, slug)

    if (!exists) {
      Console.info("Asset does not exist in this academy, creating it")
      const asset = await api.createAsset(sessionPayload.token, {
        slug: slug,
        title: learnJson.title[selectedLang],
        lang: selectedLang,
        description: learnJson.description[selectedLang],
        learnpack_deploy_url: learnpackDeployUrl,
        technologies: learnJson.technologies.map((tech: string) =>
          tech.toLowerCase().replace(/\s+/g, "-")
        ),
        url: learnpackDeployUrl,
        category: category,
        owner: user.id,
        author: user.id,
        preview: learnJson.preview,
        readme_raw: b64IndexReadme,
        all_translations,
      })
      try {
        await api.updateRigoPackage(
          sessionPayload.rigobotToken.trim(),
          learnJson.slug,
          {
            asset_id: asset.id,
          }
        )
      } catch (error) {
        Console.error("Error updating Rigo package:", error)
      }

      Console.info("Asset created with id", asset.id)
      return asset
    }

    Console.info("Asset exists, updating it")
    const asset = await api.updateAsset(sessionPayload.token, slug, {
      learnpack_deploy_url: learnpackDeployUrl,
      title: learnJson.title[selectedLang],
      category: category,
      description: learnJson.description[selectedLang],
      all_translations,
    })
    try {
      await api.updateRigoPackage(
        sessionPayload.rigobotToken.trim(),
        learnJson.slug,
        {
          asset_id: asset.id,
        }
      )
    } catch (error) {
      Console.error("Error updating Rigo package:", error)
    }

    Console.info("Asset updated with id", asset.id)
    return asset
  } catch (error) {
    Console.error("Error updating or creating asset:", error)
    return null
  }
}

const runAudit = (strict: boolean) => {
  try {
    Console.info("Running learnpack audit before publishing...")
    execSync(`learnpack audit ${strict ? "--strict" : ""}`, {
      stdio: "inherit",
    })
  } catch (error) {
    Console.error("Failed to audit with learnpack")

    // Si `execSync` lanza un error, capturamos el mensaje de error
    if (error instanceof Error) {
      Console.error(error.message)
    } else {
      Console.error("Unknown error occurred")
    }

    // Detener la ejecución del comando si `learnpack publish` falla
    process.exit(1)
  }

  // Continuar con el proceso de build solo si `learnpack publish` fue exitoso
  Console.info(
    "Learnpack publish completed successfully. Proceeding with build..."
  )
}

type Academy = {
  id: number;
  name: string;
  slug?: string;
  timezone?: string;
};

type Category = {
  id: number;
  slug: string;
  title: string;
  lang: string;
  academy: Academy;
};

function getCategoriesByAcademy(
  categories: Category[],
  academy: Academy
): Category[] {
  return categories.filter((cat) => cat.academy.id === academy.id)
}

const selectAcademy = async (
  academies: TAcademy[],
  bcToken: string
): Promise<{ academy: TAcademy | null; category: number }> => {
  if (academies.length === 0) {
    return { academy: null, category: 0 }
  }

  if (academies.length === 1) {
    return { academy: academies[0], category: 0 }
  }

  // prompts the user to  select an academy to upload the assets
  Console.info("In which academy do you want to publish the asset?")
  const response = await prompts({
    type: "select",
    name: "academy",
    message: "Select an academy",
    choices: academies.map((academy) => ({
      title: academy.name,
      value: academy,
    })),
  })

  const categories: Category[] = await api.getCategories(bcToken)
  const categoriesByAcademy = getCategoriesByAcademy(
    categories,
    response.academy
  )

  const categoriesPrompt = await prompts({
    type: "select",
    name: "category",
    message: "Select a category",
    choices: categoriesByAcademy.map((category) => ({
      title: category.title,
      value: category.id,
    })),
  })

  return {
    academy: response.academy,
    category: categoriesPrompt.category,
  }
}

class BuildCommand extends SessionCommand {
  static description =
    "Builds the project by copying necessary files and directories into a zip file"

  static flags = {
    help: flags.help({ char: "h" }),
    strict: flags.boolean({
      char: "s",
      description: "strict mode",
      default: false,
    }),
  }

  async init() {
    const { flags } = this.parse(BuildCommand)
    await this.initSession(flags)
  }

  async run() {
    const buildDir = path.join(process.cwd(), "build")

    const { flags }: { flags: { strict: boolean } } = this.parse(BuildCommand)
    const strict = flags.strict
    Console.debug("Strict mode: ", strict)
    // this.configManager?.clean()

    const configObject = this.configManager?.get()

    let sessionPayload = await SessionManager.getPayload()

    const sessionExists = sessionPayload && sessionPayload.rigobot

    const isValidToken =
      sessionExists && sessionPayload.rigobot.key ?
        await isValidRigoToken(sessionPayload.rigobot.key) :
        false

    const isValidBreathecodeToken =
      sessionExists && sessionPayload.token ?
        await api.validateToken(sessionPayload.token) :
        false

    if (!sessionExists || !isValidBreathecodeToken || !isValidToken) {
      Console.info(
        "Almost there! First you need to login with 4Geeks.com to use AI Generation tool for creators. You can create a new account here: https://4geeks.com/checkout?plan=learnpack-creator"
      )
      try {
        sessionPayload = await SessionManager.login()
      } catch (error) {
        Console.error("Error trying to authenticate")
        Console.error((error as TypeError).message || (error as string))
      }
    }

    const rigoToken = sessionPayload.rigobot.key

    const consumable = await getConsumable(
      sessionPayload.token,
      "learnpack-publish"
    )

    if (consumable.count === 0) {
      Console.error(
        "It seems you cannot publish tutorials. Make sure you creator subscription is up to date here: https://4geeks.com/profile/subscriptions. If you believe there is an issue you can always contact support@4geeks.com"
      )
      process.exit(1)
    }

    if (configObject) {
      Console.info("Cleaning configuration files")
      this.configManager?.clean()
      // build exerises
      runAudit(strict)

      Console.debug("Building exercises")
      this.configManager?.buildIndex()
    }

    const academies = await api.listUserAcademies(sessionPayload.token)

    if (academies.length === 0) {
      Console.error(
        "It seems you cannot publish tutorials. Make sure you creator subscription is up to date here: https://4geeks.com/profile/subscriptions. If you believe there is an issue you can always contact support@4geeks.com"
      )
      process.exit(1)
    }

    const { academy, category } = await selectAcademy(
      academies,
      sessionPayload.token
    )

    const learnJsonPath = path.join(process.cwd(), "learn.json")
    if (!fs.existsSync(learnJsonPath)) {
      this.error("learn.json not found")
    }

    const learnJson = JSON.parse(fs.readFileSync(learnJsonPath, "utf-8"))

    const zipFilePath = path.join(process.cwd(), `${learnJson.slug}.zip`)

    // Ensure build directory exists
    if (!fs.existsSync(buildDir)) {
      fs.mkdirSync(buildDir)
    }

    if (configObject) {
      const { config } = configObject
      const appAlreadyExists = checkIfDirectoryExists(
        `${config?.dirPath}/_app`
      )

      if (!appAlreadyExists) {
        // download app and decompress
        await downloadEditor(
          config?.editor.version,
          `${config?.dirPath}/app.tar.gz`
        )

        Console.info("Decompressing LearnPack UI, this may take a minute...")
        await decompress(
          `${config?.dirPath}/app.tar.gz`,
          `${config?.dirPath}/_app/`
        )
      }
    }

    // Copy config.json
    const configPath = path.join(process.cwd(), ".learn", "config.json")
    if (fs.existsSync(configPath)) {
      fs.copyFileSync(configPath, path.join(buildDir, "config.json"))
    } else {
      this.error("config.json not found")
    }

    // Copy .learn/assets directory, if it exists else create it
    const assetsDir = path.join(process.cwd(), ".learn", "assets")
    if (fs.existsSync(assetsDir)) {
      this.copyDirectory(assetsDir, path.join(buildDir, ".learn", "assets"))
    } else {
      fs.mkdirSync(path.join(buildDir, ".learn", "assets"), {
        recursive: true,
      })
    }

    // Copy .learn/_app directory files to the same level as config.json
    const appDir = path.join(process.cwd(), ".learn", "_app")
    if (fs.existsSync(appDir)) {
      this.copyDirectory(appDir, buildDir)
    } else {
      this.error(".learn/_app directory not found")
    }

    // After copying the _app directory
    const indexHtmlPath = path.join(appDir, "index.html")
    const buildIndexHtmlPath = path.join(buildDir, "index.html")
    const manifestPWA = path.join(appDir, "manifest.webmanifest")
    const buildManifestPWA = path.join(buildDir, "manifest.webmanifest")

    if (fs.existsSync(indexHtmlPath)) {
      let indexHtmlContent = fs.readFileSync(indexHtmlPath, "utf-8")

      const description = learnJson.description.en || "LearnPack is awesome!"
      const title =
        learnJson.title.en || "LearnPack: Interactive Learning as a Service"

      const previewUrl =
        learnJson.preview ||
        "https://raw.githubusercontent.com/learnpack/ide/refs/heads/master/public/learnpack.svg"
      // Replace placeholders and the <title>Old title </title> tag for a new tag with the title
      indexHtmlContent = indexHtmlContent
        .replace(/{{description}}/g, description)
        .replace(/<title>.*<\/title>/, `<title>${title}</title>`)
        .replace(/{{title}}/g, title)
        .replace(/{{preview}}/g, previewUrl)
        .replace(/{{slug}}/g, learnJson.slug)
        .replace(/{{duration}}/g, minutesToISO8601Duration(learnJson.duration))

      // Write the modified content to the build directory
      fs.writeFileSync(buildIndexHtmlPath, indexHtmlContent)
    } else {
      this.error("index.html not found in _app directory")
    }

    if (fs.existsSync(manifestPWA)) {
      let manifestPWAContent = fs.readFileSync(manifestPWA, "utf-8")
      manifestPWAContent = manifestPWAContent.replace(
        "{{course_title}}",
        learnJson.title.us
      )

      const courseShortName = { answer: "testing-tutorial" }
      // const courseShortName = await generateCourseShortName(rigoToken, {
      //   learnJSON: JSON.stringify(learnJson),
      // })

      manifestPWAContent = manifestPWAContent.replace(
        "{{course_app_name}}",
        courseShortName.answer
      )
      fs.writeFileSync(buildManifestPWA, manifestPWAContent)
    } else {
      this.error("manifest.webmanifest not found in _app directory")
    }

    // Copy exercises directory
    const exercisesDir = path.join(process.cwd(), "exercises")
    const learnExercisesDir = path.join(process.cwd(), ".learn", "exercises")

    if (fs.existsSync(exercisesDir)) {
      this.copyDirectory(exercisesDir, path.join(buildDir, "exercises"))
    } else if (fs.existsSync(learnExercisesDir)) {
      this.copyDirectory(learnExercisesDir, path.join(buildDir, "exercises"))
    } else {
      this.error("exercises directory not found in either location")
    }

    fs.copyFileSync(learnJsonPath, path.join(buildDir, "learn.json"))
    const sidebarPath = path.join(process.cwd(), ".learn", "sidebar.json")
    if (fs.existsSync(sidebarPath)) {
      fs.copyFileSync(sidebarPath, path.join(buildDir, "sidebar.json"))
    } else {
      this.error("sidebar.json not found in .learn directory")
    }

    const output = fs.createWriteStream(zipFilePath)
    const archive = archiver("zip", {
      zlib: { level: 9 },
    })

    output.on("close", async () => {
      this.log(
        `Build completed: ${zipFilePath} (${archive.pointer()} total bytes)`
      )
      // Remove build directory after zip is created

      Console.debug("Zip file saved in project root")

      const formData = new FormData()
      formData.append("file", fs.createReadStream(zipFilePath))
      formData.append("config", JSON.stringify(learnJson))

      try {
        const res = await axios.post(uploadZipEndpont, formData, {
          headers: {
            ...formData.getHeaders(),
            Authorization: `Token ${rigoToken}`,
          },
        })
        console.log(res.data)

        fs.unlinkSync(zipFilePath)
        this.removeDirectory(buildDir)

        await handleAssetCreation(
          sessionPayload,
          learnJson,
          "en",
          res.data.url,
          "",
          []
        )
      } catch (error) {
        if (axios.isAxiosError(error)) {
          if (error.response && error.response.status === 403) {
            console.error("Error 403:", error.response.data.error)
          } else if (error.response && error.response.status === 400) {
            console.error(error.response.data.error)
          } else {
            console.error("Error uploading file:", error)
          }
        } else {
          console.error("Error uploading file:", error)
        }

        // fs.unlinkSync(zipFilePath)
        // this.removeDirectory(buildDir)
      }
    })

    archive.on("error", (err: any) => {
      throw err
    })

    archive.pipe(output)
    archive.directory(buildDir, false)
    await archive.finalize()
  }

  copyDirectory(src: string, dest: string) {
    if (!fs.existsSync(dest)) {
      fs.mkdirSync(dest, { recursive: true })
    }

    const entries = fs.readdirSync(src, { withFileTypes: true })

    for (const entry of entries) {
      const srcPath = path.join(src, entry.name)
      const destPath = path.join(dest, entry.name)

      if (entry.isDirectory()) {
        this.copyDirectory(srcPath, destPath)
      } else {
        fs.copyFileSync(srcPath, destPath)
      }
    }
  }

  removeDirectory(dir: string) {
    if (fs.existsSync(dir)) {
      fs.readdirSync(dir).forEach((file) => {
        const currentPath = path.join(dir, file)
        if (fs.lstatSync(currentPath).isDirectory()) {
          this.removeDirectory(currentPath)
        } else {
          fs.unlinkSync(currentPath)
        }
      })
      fs.rmdirSync(dir)
    }
  }
}

BuildCommand.flags = {
  strict: flags.boolean({
    char: "s",
    description: "strict mode",
    default: false,
  }),
  help: flags.help({ char: "h" }),
}

export default BuildCommand
