import Console from "../utils/console"
import * as storage from "node-persist"
import cli from "cli-ux"
import axios from "axios"
import * as dotenv from "dotenv"

dotenv.config()

const HOST = "https://breathecode.herokuapp.com"
export const RIGOBOT_HOST = "https://rigobot.herokuapp.com"
export const RIGOBOT_REALTIME_HOST = "https://ai.4geeks.com"
// export const RIGOBOT_REALTIME_HOST = "http://127.0.0.1:8003"
// export const RIGOBOT_HOST = "https://rigobot-test-cca7d841c9d8.herokuapp.com"
// export const RIGOBOT_HOST =
// "https://8000-charlytoc-rigobot-bmwdeam7cev.ws-us118.gitpod.io"

// eslint-disable-next-line
const _fetch = require("node-fetch");

interface IHeaders {
  "Content-Type"?: string;
  Authorization?: string;
}

interface IOptions {
  headers?: IHeaders;
  method?: string;
  body?: string;
}

const fetch = async (
  url: string,
  options: IOptions = {},
  returnAsJson = true
) => {
  const headers: IHeaders = { "Content-Type": "application/json" }
  Console.debug(`Fetching ${url}`)
  let session = null
  try {
    session = await storage.getItem("bc-payload")
    if (session.token && session.token !== "" && !url.includes("/token"))
      headers.Authorization = "Token " + session.token
  } catch {}

  try {
    const resp = await _fetch(url, {
      ...options,
      headers: { ...headers, ...options.headers },
    } as any)

    if (resp.status >= 200 && resp.status < 300) {
      return returnAsJson ? await resp.json() : await resp.text()
    }

    if (resp.status === 401)
      Console.debug("Invalid authentication credentials", `Code: 401`)
    // throw APIError("Invalid authentication credentials", 401)
    else if (resp.status === 404) throw APIError("Package not found", 404)
    else if (resp.status >= 500)
      throw APIError("Impossible to connect with the server", 500)
    else if (resp.status >= 400) {
      const error = await resp.json()
      if (error.detail || error.error) {
        throw APIError(error.detail || error.error)
      } else if (error.nonFieldErrors) {
        throw APIError(error.nonFieldErrors[0], error)
      } else if (typeof error === "object") {
        if (Object.keys(error).length > 0) {
          const key = error[Object.keys(error)[0]]
          throw APIError(`${key}: ${error[key][0]}`, error)
        }
      } else {
        throw APIError("Uknown error")
      }
    } else throw APIError("Uknown error")
  } catch (error) {
    Console.error((error as TypeError).message)
    throw error
  }
}

const login = async (identification: string, password: string) => {
  try {
    cli.action.start(`Looking for credentials with ${identification}`)
    await cli.wait(1000)
    const url = `${HOST}/v1/auth/login/`

    const res = await axios.post(url, {
      email: identification,
      password: password,
    })
    const data = res.data

    cli.action.stop("ready")
    let rigoPayload = null
    try {
      rigoPayload = await loginRigo(data.token)
    } catch {
      return { ...data, rigobot: null }
    }

    return { ...data, rigobot: rigoPayload }
  } catch (error) {
    cli.action.stop("error")
    Console.error((error as TypeError).message)
    Console.debug(error)
  }
}

const loginRigo = async (token: string) => {
  try {
    const rigoUrl = `${RIGOBOT_HOST}/v1/auth/me/token?breathecode_token=${token}`
    const rigoResp = await _fetch(rigoUrl)
    const rigobotJson = await rigoResp.json()
    return rigobotJson
  } catch (error) {
    // Handle the error as needed, for example log it or return a custom error message
    Console.error(
      "Error logging in to Rigo, did you already accepted Rigobot?:",
      error
    )
    throw new Error("Failed to log in to Rigo")
  }
}

const publish = async (config: any) => {
  const keys = [
    "difficulty",
    "language",
    "skills",
    "technologies",
    "slug",
    "repository",
    "author",
    "title",
  ]

  const payload: { [key: string]: string } = {}
  for (const k of keys) config[k] ? (payload[k] = config[k]) : null
  try {
    console.log("Package to publish:", payload)
    cli.action.start("Updating package information...")
    await cli.wait(1000)
    const data = await fetch(`${HOST}/v1/package/${config.slug}`, {
      method: "PUT",
      body: JSON.stringify(payload),
    })
    cli.action.stop("ready")
    return data
  } catch (error) {
    Console.error((error as TypeError).message)
    Console.debug(error)
    throw error
  }
}

const update = async (config: any) => {
  try {
    cli.action.start("Updating package information...")
    await cli.wait(1000)
    const data = await fetch(`${HOST}/v1/package/`, {
      method: "POST",
      body: JSON.stringify(config),
    })
    cli.action.stop("ready")
    return data
  } catch (error) {
    Console.error((error as any).message)
    Console.debug(error)
    throw error
  }
}

const getPackage = async (slug: string) => {
  try {
    cli.action.start("Downloading package information...")
    await cli.wait(1000)
    const data = await fetch(`${HOST}/v1/package/${slug}`)
    cli.action.stop("ready")
    return data
  } catch (error) {
    if ((error as any).status === 404)
      Console.error(`Package ${slug} does not exist`)
    else Console.error(`Package ${slug} does not exist`)
    Console.debug(error)
    throw error
  }
}

const getLangs = async () => {
  try {
    cli.action.start("Downloading language options...")
    await cli.wait(1000)
    const data = await fetch(`${HOST}/v1/package/language`)
    cli.action.stop("ready")
    return data
  } catch (error) {
    if ((error as any).status === 404)
      Console.error("Package slug does not exist")
    else Console.error("Package slug does not exist")
    Console.debug(error)
    throw error
  }
}

const getAllPackages = async ({
  lang = "",
  slug = "",
}: {
  lang?: string;
  slug?: string;
}) => {
  try {
    cli.action.start("Downloading packages...")
    await cli.wait(1000)
    const data = await fetch(
      `${HOST}/v1/package/all?limit=100&language=${lang}&slug=${slug}`
    )
    cli.action.stop("ready")
    return data
  } catch (error) {
    Console.error(`Package ${slug} does not exist`)
    Console.debug(error)
    throw error
  }
}

const APIError = (error: TypeError | string, code?: number) => {
  const message: string = (error as TypeError).message || (error as string)
  const _err = new Error(message) as any
  _err.status = code || 400
  return _err
}

const sendBatchTelemetry = async function (url: string, body: any) {
  if (!url) {
    return
  }

  const session = await storage.getItem("bc-payload")
  if (
    !session ||
    !Object.prototype.hasOwnProperty.call(session, "token") ||
    session.token === ""
  ) {
    Console.debug("No token found, skipping batch telemetry delivery")
    return
  }

  if (!session || !session.user_id || session.user_id === "") {
    Console.debug("No user_id found, skipping batch telemetry delivery")
    return
  }

  body.user_id = session.user_id

  fetch(
    url,
    {
      method: "POST",
      body: JSON.stringify(body),
    },
    false
  )
    .then(response => {
      Console.debug("Telemetry sent successfully")
      return response
    })
    .catch(error => {
      Console.debug("Error while sending batch Telemetry", error)
    })
}

const sendStreamTelemetry = async function (url: string, body: object) {
  if (!url) {
    return
  }

  const session = await storage.getItem("bc-payload")
  if (
    !session ||
    !Object.prototype.hasOwnProperty.call(session, "token") ||
    session.token === ""
  ) {
    Console.debug("No token found, skipping stream telemetry delivery")
    return
  }

  fetch(
    url,
    {
      method: "POST",
      body: JSON.stringify(body),
    },
    false
  )
    .then(response => {
      return response
    })
    .catch(error => {
      Console.debug("Error while sending stream Telemetry", error)
    })
}

type TConsumableSlug =
  | "ai-conversation-message"
  | "ai-compilation"
  | "ai-tutorial-generation"
  | "ai-generation"
  | "learnpack-publish";

export const countConsumables = (
  consumables: any,
  consumableSlug: TConsumableSlug = "ai-tutorial-generation"
) => {
  // Find the void that matches the consumableSlug

  const consumable = consumables.voids.find(
    (voidItem: any) => voidItem.slug === consumableSlug
  )

  // Return the available units or 0 if not found
  return consumable ? consumable.balance.unit : 0
}

export const getConsumable = async (
  token: string,
  consumableSlug: TConsumableSlug = "ai-generation"
): Promise<any> => {
  const url = `${HOST}/v1/payments/me/service/consumable?virtual=true`

  const headers = {
    Authorization: `Token ${token}`,
  }

  try {
    const response = await axios.get(url, { headers })

    const count = countConsumables(response.data, consumableSlug)

    return { count }
  } catch (error) {
    console.error("Error fetching consumables:", error)
    throw error
  }
}

export interface TAcademy {
  id: number;
  name: string;
  slug: string;
  timezone: string;
}

const neededPermissions = [
  "add_asset",
  "change_asset",
  "view_asset",
  "delete_asset",
]

export const listUserAcademies = async (
  breathecodeToken: string
): Promise<TAcademy[]> => {
  const url = "https://breathecode.herokuapp.com/v1/auth/user/me"

  try {
    const response = await axios.get(url, {
      headers: {
        Authorization: `Token ${breathecodeToken}`,
      },
    })

    const data = response.data

    const academiesMap = new Map<number, TAcademy>()

    for (const role of data.roles) {
      const academy = role.academy
      if (!academiesMap.has(academy.id)) {
        academiesMap.set(academy.id, academy)
      }
    }

    const permissions = new Set(data.permissions.map((p: any) => p.codename))

    // Validate if the user has ALL the needed permissions
    const hasAllPermissions = neededPermissions.every(permission =>
      permissions.has(permission)
    )

    if (!hasAllPermissions) {
      // The user does not have all the needed permissions

      return []
    }

    return [...academiesMap.values()]
  } catch (error) {
    console.error("Failed to fetch user academies:", error)
    return []
  }
}

export const validateToken = async (token: string) => {
  const url = "https://breathecode.herokuapp.com/v1/auth/user/me"
  const headers = {
    Authorization: `Token ${token}`,
  }

  try {
    const response = await axios.get(url, { headers })
    return response.data
  } catch {
    return false
  }
}

type TAssetMissing = {
  slug: string;
  title: string;
  lang: string;
  url: string;
  description: string;
  learnpack_deploy_url: string;
  technologies: string[];
  category: number | string;
  owner: number;
  author: number;
  preview: string;
  readme_raw: string;
  all_translations: string[];
};

export const createAsset = async (token: string, asset: TAssetMissing) => {
  const body = {
    slug: asset.slug,
    title: asset.title,
    lang: asset.lang,
    asset_type: "EXERCISE",
    visibility: "PUBLIC",
    status: "DRAFT",
    url: asset.url,
    readme_url: null,
    difficulty: null,
    duration: null,
    graded: true,
    gitpod: true,
    category: asset.category,
    owner: asset.owner,
    author: asset.author,
    preview: asset.preview,
    description: asset.description,
    external: false,
    interactive: true,
    solution_video_url: null,
    intro_video_url: null,
    translations: [asset.lang],
    learnpack_deploy_url: asset.learnpack_deploy_url,
    technologies: asset.technologies,
    readme_raw: asset.readme_raw,
    all_translations: asset.all_translations,
  }
  const url = `https://breathecode.herokuapp.com/v1/registry/asset/me`
  const headers = {
    Authorization: `Token ${token}`,
  }

  try {
    const response = await axios.post(url, body, { headers })
    return response.data
  } catch (error: any) {
    console.error("Failed to create asset:", error)
    throw error.response.data
  }
}

export const doesAssetExists = async (
  token: string,
  assetSlug: string
): Promise<{ exists: boolean; academyId?: number }> => {
  const url = `https://breathecode.herokuapp.com/v1/registry/asset/${assetSlug}`

  const headers = {
    Authorization: `Token ${token}`,
  }

  try {
    const response = await axios.get(url, { headers })
    if (response.status === 200) {
      return { exists: true }
    }

    return { exists: false }
  } catch {
    // console.error("Failed to get asset:", error)
    return { exists: false }
  }
}

const updateAsset = async (
  token: string,
  assetSlug: string,
  asset: Partial<TAssetMissing>
) => {
  const url = `https://breathecode.herokuapp.com/v1/registry/asset/me/${assetSlug}`
  const headers = {
    Authorization: `Token ${token}`,
  }
  try {
    const response = await axios.put(url, asset, { headers })
    return response.data
  } catch (error: any) {
    console.error("Failed to update asset:", error)
    // Try to print the data
    // console.log(error.response.data)
    throw error.response.data
  }
}

const getCategories = async (token: string) => {
  const url = `${HOST}/v1/registry/category`
  const headers = {
    Authorization: `Token ${token}`,
  }
  try {
    const response = await axios.get(url, { headers })
    return response.data
  } catch (error) {
    console.error("Failed to get categories:", error)
    throw error
  }
}

const updateRigoPackage = async (
  token: string,
  slug: string,
  updates: { asset_id?: number; new_slug?: string }
) => {
  const cleanToken = token.replace(/[\n\r]/g, "")

  const url = `${RIGOBOT_HOST}/v1/learnpack/package/${slug}/`

  try {
    const response = await axios.put(url, updates, {
      headers: {
        Authorization: "Token " + cleanToken,
      },
    })
    return response.data
  } catch (error) {
    console.error("Failed to update Rigo package:", error)
    throw error
  }
}

const createRigoPackage = async (token: string, slug: string, config: any) => {
  const url = `${RIGOBOT_HOST}/v1/learnpack/package`
  const cleanToken = token.replace(/[\n\r]/g, "")

  try {
    const response = await axios.post(
      url,
      { slug, config },
      {
        headers: {
          Authorization: "Token " + cleanToken,
        },
      }
    )
    return response.data
  } catch (error) {
    console.error("Failed to create Rigo package:", error)
    throw error
  }
}

type TTechnology = {
  slug: string;
  lang: string;
};

let technologiesCache: TTechnology[] = []

export const fetchTechnologies = async () => {
  const BREATHECODE_PERMANENT_TOKEN = process.env.BREATHECODE_PERMANENT_TOKEN
  const LANGS = ["en", "es"]

  if (!BREATHECODE_PERMANENT_TOKEN) {
    throw new Error(
      "BREATHECODE_PERMANENT_TOKEN is not defined in environment variables"
    )
  }

  const headers = {
    Authorization: `Token ${BREATHECODE_PERMANENT_TOKEN}`,
  }

  const results = await Promise.all(
    LANGS.map(lang =>
      axios
        .get(`${HOST}/v1/registry/technology?lang=${lang}`, { headers })
        .then(res => {
          return res.data
        })
        .then(data =>
          data.map((item: any) => ({
            slug: item.slug,
            lang: lang,
          }))
        )
    )
  )

  const allItems = results.flat()

  // Remove duplicates by slug+lang combination
  const unique = []
  const seen = new Set()
  for (const item of allItems) {
    const key = `${item.slug}:${item.lang}`
    if (!seen.has(key)) {
      seen.add(key)
      unique.push(item)
    }
  }

  return unique
}

// Function to update the cache and schedule the next update
async function updateTechnologiesPeriodically() {
  try {
    technologiesCache = await fetchTechnologies()
    // Uncomment for debugging:
    // console.log('Technologies list updated:', technologiesCache);
  } catch (error: any) {
    console.error("Error updating technologies list:", error)
  } finally {
    setTimeout(updateTechnologiesPeriodically, 24 * 60 * 60 * 1000)
  }
}

export const getCurrentTechnologies = () => technologiesCache

export default {
  login,
  publish,
  update,
  getPackage,
  getLangs,
  getAllPackages,
  sendBatchTelemetry,
  sendStreamTelemetry,
  listUserAcademies,
  validateToken,
  createAsset,
  doesAssetExists,
  updateAsset,
  getCategories,
  updateRigoPackage,
  createRigoPackage,
  getCurrentTechnologies,
  updateTechnologiesPeriodically,
}
