import { CookieJarService } from "@hoppscotch/common/services/cookie-jar.service"
import { Interceptor, InterceptorError, RequestRunResult } from "@hoppscotch/common/services/interceptor.service"
import { Service } from "dioc"
import { cloneDeep } from "lodash-es"
import { invoke } from "@tauri-apps/api/tauri"
import * as E from "fp-ts/Either"
import SettingsNativeInterceptor from "../../../components/settings/NativeInterceptor.vue"
import { ref, watch } from "vue"
import { z } from "zod"
import { PersistenceService } from "@hoppscotch/common/services/persistence"
import { CACertStore, ClientCertsStore, ClientCertStore, StoredClientCert } from "./persisted-data"


type KeyValuePair = {
  key: string,
  value: string
}

type FormDataValue =
  | { Text: string }
  | {
      File: {
        filename: string,
        data: number[],
        mime: string
      }
    }

type FormDataEntry = {
  key: string,
  value: FormDataValue
}

type BodyDef =
  | { Text: string }
  | { URLEncoded: KeyValuePair[] }
  | { FormData: FormDataEntry[] }

type ClientCertDef =
  | {
      PEMCert: {
        certificate_pem: number[],
        key_pem: number[]
      },
    }
  | {
      PFXCert: {
        certificate_pfx: number[],
        password: string
      }
    }

// TODO: Figure out a way to autogen this from the interceptor definition on the Rust side
export type RequestDef = {
  req_id: number

  method: string
  endpoint: string

  parameters: KeyValuePair[]
  headers: KeyValuePair[]

  body: BodyDef | null,

  validate_certs: boolean,
  root_cert_bundle_files: number[],
  client_cert: ClientCertDef | null

  proxy?: {
    url: string
  }
}

type RunRequestResponse = {
  status: number,
  status_text: string,
  headers: KeyValuePair[],
  data: number[],

  time_start_ms: number,
  time_end_ms: number
}

// HACK: To solve the AxiosRequestConfig being different between @hoppscotch/common
// and the axios present in this package
type AxiosRequestConfig = Parameters<Interceptor["runRequest"]>[0]

export const preProcessRequest = (
  req: AxiosRequestConfig
): AxiosRequestConfig => {
  const reqClone = cloneDeep(req)

  // If the parameters are URLSearchParams, inject them to URL instead
  // This prevents issues of marshalling the URLSearchParams to the proxy
  if (reqClone.params instanceof URLSearchParams) {
    try {
      const url = new URL(reqClone.url ?? "")

      for (const [key, value] of reqClone.params.entries()) {
        url.searchParams.append(key, value)
      }

      reqClone.url = url.toString()
    } catch (e) {
      // making this a non-empty block, so we can make the linter happy.
      // we should probably use, allowEmptyCatch, or take the time to do something with the caught errors :)
    }

    reqClone.params = {}
  }

  return reqClone
}

async function processBody(axiosReq: AxiosRequestConfig): Promise<BodyDef | null> {
  if (!axiosReq.data) return null

  if (typeof axiosReq.data === "string") {
    return { Text: axiosReq.data }
  }

  if (axiosReq.data instanceof FormData) {
    const entries: FormDataEntry[] = []

    for (const [key, value] of axiosReq.data.entries()) {
      if (typeof value === "string") {
        entries.push({
          key,
          value: { Text: value }
        })
      } else {
        const mime = value.type !== "" ? value.type : "application/octet-stream"

        entries.push({
          key,
          value: {
            File: {
              filename: value.name,
              data: Array.from(new Uint8Array(await value.arrayBuffer())),
              mime,
            }
          }
        })
      }
    }

    return { FormData: entries }
  }

  throw new Error("Native Process Body: Unhandled Axios Request Configuration")
}

function getURLDomain(url: string): string | null {
  try {
    return new URL(url).host
  } catch (_) {
    return null
  }
}

function convertClientCertToDefCert(cert: ClientCertificateEntry): ClientCertDef {
  if ("PEMCert" in cert.cert) {
    return {
      PEMCert: {
        certificate_pem: Array.from(cert.cert.PEMCert.certificate_pem),
        key_pem: Array.from(cert.cert.PEMCert.key_pem)
      }
    }
  } else {
    return {
      PFXCert: {
        certificate_pfx: Array.from(cert.cert.PFXCert.certificate_pfx),
        password: cert.cert.PFXCert.password
      }
    }
  }
}

async function convertToRequestDef(
  axiosReq: AxiosRequestConfig,
  reqID: number,
  caCertificates: CACertificateEntry[],
  clientCertificates: Map<string, ClientCertificateEntry>,
  validateCerts: boolean,
  proxyInfo: RequestDef["proxy"]
): Promise<RequestDef> {
  const clientCertDomain = getURLDomain(axiosReq.url!)

  const clientCert = clientCertDomain ? clientCertificates.get(clientCertDomain) : null

  return {
    req_id: reqID,
    method: axiosReq.method ?? "GET",
    endpoint: axiosReq.url ?? "",
    headers: Object.entries(axiosReq.headers ?? {})
      .filter(
        ([key, value]) =>
          !(
            key.toLowerCase() === "content-type" &&
            value.toLowerCase() === "multipart/form-data"
          )
      ) // Removing header, because this header will be set by relay.
      .map(([key, value]): KeyValuePair => ({ key, value })),
    parameters: Object.entries(axiosReq.params as Record<string, string> ?? {})
      .map(([key, value]): KeyValuePair => ({ key, value })),
    body: await processBody(axiosReq),
    root_cert_bundle_files: caCertificates.map((cert) => Array.from(cert.certificate)),
    validate_certs: validateCerts,
    client_cert: clientCert ? convertClientCertToDefCert(clientCert) : null,
    proxy: proxyInfo
  }
}

export const CACertificateEntry = z.object({
  filename: z.string().min(1),
  enabled: z.boolean(),
  certificate: z.instanceof(Uint8Array)
})

export type CACertificateEntry = z.infer<typeof CACertificateEntry>

export const ClientCertificateEntry = z.object({
  enabled: z.boolean(),
  domain: z.string().trim().min(1),
  cert: z.union([
    z.object({
      PEMCert: z.object({
        certificate_filename: z.string().min(1),
        certificate_pem: z.instanceof(Uint8Array),

        key_filename: z.string().min(1),
        key_pem: z.instanceof(Uint8Array),
      })
    }),
    z.object({
      PFXCert: z.object({
        certificate_filename: z.string().min(1),
        certificate_pfx: z.instanceof(Uint8Array),

        password: z.string()
      })
    })
  ])
})

export type ClientCertificateEntry = z.infer<typeof ClientCertificateEntry>

const CA_STORE_PERSIST_KEY = "native_interceptor_ca_store"
const CLIENT_CERTS_PERSIST_KEY = "native_interceptor_client_certs_store"
const VALIDATE_SSL_KEY = "native_interceptor_validate_ssl"
const PROXY_INFO_PERSIST_KEY = "native_interceptor_proxy_info"

export class NativeInterceptorService extends Service implements Interceptor {
  public static readonly ID = "NATIVE_INTERCEPTOR_SERVICE"

  public interceptorID = "native"

  public name = () => "Native"

  public selectable = { type: "selectable" as const }

  public supportsCookies = true
  public supportsDigestAuth = true
  public supportsBinaryContentType = false

  private cookieJarService = this.bind(CookieJarService)
  private persistenceService: PersistenceService = this.bind(PersistenceService)

  private reqIDTicker = 0

  public settingsPageEntry = {
    entryTitle: () => "Native", // TODO: i18n this
    component: SettingsNativeInterceptor
  }

  public caCertificates = ref<CACertificateEntry[]>([])

  public clientCertificates = ref<Map<string, ClientCertificateEntry>>(new Map())
  public validateCerts = ref(true)
  public proxyInfo = ref<RequestDef["proxy"]>(undefined)

  override onServiceInit() {
    // Load SSL Validation
    const persistedValidateSSL: unknown = JSON.parse(
      this.persistenceService.getLocalConfig(VALIDATE_SSL_KEY) ?? "null"
    )

    if (typeof persistedValidateSSL === "boolean") {
      this.validateCerts.value = persistedValidateSSL
    }

    const persistedProxyInfo = this.persistenceService.getLocalConfig(
      PROXY_INFO_PERSIST_KEY
    )

    if (persistedProxyInfo && persistedProxyInfo !== "null") {
      try {
        const proxyInfo = JSON.parse(persistedProxyInfo)
        this.proxyInfo.value = proxyInfo
      } catch (e) {}
    }

    watch(this.validateCerts, () => {
      this.persistenceService.setLocalConfig(VALIDATE_SSL_KEY, JSON.stringify(this.validateCerts.value))
    })

    // Load and setup writes for CA Store
    const persistedCAStoreData = JSON.parse(
      this.persistenceService.getLocalConfig(CA_STORE_PERSIST_KEY) ?? "null"
    )

    const caStoreDataParseResult = CACertStore.safeParse(persistedCAStoreData)

    if (caStoreDataParseResult.type === "ok") {
      this.caCertificates.value = caStoreDataParseResult.value.certs
        .map((entry) => ({ ...entry, certificate: new Uint8Array(entry.certificate) }))
    }

    watch(this.caCertificates, (certs) => {
      const storableValue: CACertStore = {
        v: 1,
        certs: certs
          .map((el) => ({ ...el, certificate: Array.from(el.certificate) }))
      }

      this.persistenceService.setLocalConfig(CA_STORE_PERSIST_KEY, JSON.stringify(storableValue))
    })


    // Load and setup writes for Client Certs Store
    const persistedClientCertStoreData = JSON.parse(
      this.persistenceService.getLocalConfig(CLIENT_CERTS_PERSIST_KEY) ?? "null"
    )

    const clientCertStoreDataParseResult = ClientCertsStore.safeParse(persistedClientCertStoreData)

    if (clientCertStoreDataParseResult.type === "ok") {
      this.clientCertificates.value = new Map(
        Object.entries(
          clientCertStoreDataParseResult.value.clientCerts
        )
        .map(([domain, cert]) => {
          if ("PFXCert" in cert.cert) {
            const newCert = <ClientCertificateEntry>{
              ...cert,
              cert: {
                PFXCert: {
                  certificate_pfx: new Uint8Array(cert.cert.PFXCert.certificate_pfx),
                  certificate_filename: cert.cert.PFXCert.certificate_filename,

                  password: cert.cert.PFXCert.password
                }
              }
            }

            return [domain, newCert]
          } else {
            const newCert = <ClientCertificateEntry>{
              ...cert,
              cert: {
                PEMCert: {
                  certificate_pem: new Uint8Array(cert.cert.PEMCert.certificate_pem),
                  certificate_filename: cert.cert.PEMCert.certificate_filename,

                  key_pem: new Uint8Array(cert.cert.PEMCert.key_pem),
                  key_filename: cert.cert.PEMCert.key_filename
                }
              }
            }

            return [domain, newCert]
          }
        })
      )
    }

    watch(this.clientCertificates, (certs) => {
      const storableValue: ClientCertStore = {
        v: 1,
        clientCerts: Object.fromEntries(
          Array.from(certs.entries())
            .map(([domain, cert]) => {
              if ("PFXCert" in cert.cert) {
                const newCert = <StoredClientCert>{
                  ...cert,
                  cert: {
                    PFXCert: {
                      certificate_pfx: Array.from(cert.cert.PFXCert.certificate_pfx),
                      certificate_filename: cert.cert.PFXCert.certificate_filename,

                      password: cert.cert.PFXCert.password
                    }
                  }
                }

                return [domain, newCert]
              } else {
                const newCert = <StoredClientCert>{
                  ...cert,
                  cert: {
                    PEMCert: {
                      certificate_pem: Array.from(cert.cert.PEMCert.certificate_pem),
                      certificate_filename: cert.cert.PEMCert.certificate_filename,

                      key_pem: Array.from(cert.cert.PEMCert.key_pem),
                      key_filename: cert.cert.PEMCert.key_filename
                    }
                  }
                }

                return [domain, newCert]
              }
            })
        )
      }


      this.persistenceService.setLocalConfig(CLIENT_CERTS_PERSIST_KEY, JSON.stringify(storableValue))
    })

    watch(this.proxyInfo, (newProxyInfo) => {
      this.persistenceService.setLocalConfig(
        PROXY_INFO_PERSIST_KEY,
        JSON.stringify(newProxyInfo) ?? "null"
      )
    })
  }

  public runRequest(req: AxiosRequestConfig): RequestRunResult<InterceptorError> {
    const processedReq = preProcessRequest(req)

    const relevantCookies = this.cookieJarService.getCookiesForURL(
      new URL(processedReq.url!)
    )

    if (relevantCookies.length > 0) {
      processedReq.headers["Cookie"] = relevantCookies
        .map((cookie) => `${cookie.name!}=${cookie.value!}`)
        .join(";")
    }

    const reqID = this.reqIDTicker++;

    return {
      cancel: () => {
        invoke("plugin:hopp_native_interceptor|cancel_request", { reqId: reqID });
      },
      response: (async () => {
        const requestDef = await convertToRequestDef(
          processedReq,
          reqID,
          this.caCertificates.value,
          this.clientCertificates.value,
          this.validateCerts.value,
          this.proxyInfo.value
        )

        try {

          const response: RunRequestResponse = await invoke(
            "plugin:hopp_native_interceptor|run_request",
            { req: requestDef }
          )

          return E.right({
            headers: Object.fromEntries(
              response.headers.map(({ key, value }) => [key, value])
            ),
            status: response.status,
            statusText: response.status_text,
            data: new Uint8Array(response.data).buffer,
            config: {
              timeData: {
                startTime: response.time_start_ms,
                endTime: response.time_end_ms
              }
            },
            additional: {
              multiHeaders: response.headers
            }
          })
        } catch (e) {

          if (typeof e === "object" && (e as any)["RequestCancelled"]) {
            return E.left("cancellation" as const)
          }

          // TODO: More in-depth error messages
          return E.left(<InterceptorError>{
            humanMessage: {
              heading: (t) => t("error.network_fail"),
              description: (t) => t("helpers.network_fail"),
            }
          })
        }
      })()
    }
  }
}
