import { CookieStore, RequestCookieStore } from "@worker-tools/request-cookie-store";
import { SignedCookieStore, DeriveOptions } from "@worker-tools/signed-cookie-store";
import { EncryptedCookieStore } from "@worker-tools/encrypted-cookie-store";
import { ResolvablePromise } from '@worker-tools/resolvable-promise';
import { forbidden } from "@worker-tools/response-creators";

import { Awaitable } from "./utils/common-types.js";
import { MiddlewareCookieStore } from "./utils/middleware-cookie-store.js";
import { headersSetCookieFix } from './utils/headers-set-cookie-fix.js'
import { unsettle } from "./utils/unsettle.js";
import { Context } from "./index.js";

export async function cookiesFrom(cookieStore: CookieStore): Promise<Cookies> {
  return Object.fromEntries((await cookieStore.getAll()).map(({ name, value }) => [name, value]));
}

/**
 * An object of the cookies sent with this request.
 * It is for reading convenience only.
 * To make changes, use the associated cookie store instead (provided by the middleware along with this object)
 */
export type Cookies = { readonly [key: string]: string };

export interface CookiesContext {
  cookieStore: CookieStore, 
  cookies: Cookies, 
}
export interface UnsignedCookiesContext extends CookiesContext { 
  unsignedCookieStore: CookieStore, 
  unsignedCookies: Cookies 
}
export interface SignedCookiesContext extends CookiesContext { 
  signedCookieStore: CookieStore, 
  signedCookies: Cookies,
}
export interface EncryptedCookiesContext extends CookiesContext { 
  encryptedCookieStore: CookieStore, 
  encryptedCookies: Cookies,
}

export interface CookiesOptions extends DeriveOptions {
  keyring?: readonly CryptoKey[];
}

export const plainCookies = () => async <X extends Context>(ax: Awaitable<X>): Promise<X & UnsignedCookiesContext> => {
  const x = await ax;
  const cookieStore = new RequestCookieStore(x.request);
  const requestDuration = new ResolvablePromise<void>();
  const unsignedCookieStore = new MiddlewareCookieStore(cookieStore, requestDuration)
  const unsignedCookies = await cookiesFrom(unsignedCookieStore);
  const nx = Object.assign(x, { 
    cookieStore: unsignedCookieStore, 
    cookies: unsignedCookies, 
    unsignedCookieStore, 
    unsignedCookies, 
  })
  x.effects.push(response => {
    requestDuration.resolve();
    const { headers: cookieHeaders } = cookieStore
    if (cookieHeaders.length) response.headers.append('VARY', 'Cookie')
    const { status, statusText, headers, body } = response
    return new Response(body, {
      status,
      statusText,
      headers: [
        ...headersSetCookieFix(headers),
        ...cookieHeaders,
      ],
    });
  })
  return nx;
}

export { plainCookies as unsignedCookies }

export const signedCookies = (opts: CookiesOptions) => {
  // TODO: options to provide own cryptokey??
  // TODO: What if secret isn't known at initialization (e.g. Cloudflare Workers)
  if (!opts.secret) throw TypeError('Secret missing');

  const keyPromise = SignedCookieStore.deriveCryptoKey(opts);

  return async <X extends Context>(ax: Awaitable<X>): Promise<X & SignedCookiesContext> => {
    const x = await ax;
    const request = x.request;
    const cookieStore = new RequestCookieStore(request);
    const requestDuration = new ResolvablePromise<void>();
    const signedCookieStore = new MiddlewareCookieStore(new SignedCookieStore(cookieStore, await keyPromise, {
      keyring: opts.keyring
    }), requestDuration);

    let signedCookies: Cookies;
    try {
      signedCookies = await cookiesFrom(signedCookieStore);
    } catch {
      throw forbidden();
    }

    const nx = Object.assign(x, {
      cookieStore: signedCookieStore,
      cookies: signedCookies,
      signedCookieStore,
      signedCookies,
    })

    x.effects.push(async response => {
      // Wait for all set cookie promises to settle
      requestDuration.resolve();
      await unsettle(signedCookieStore.allSettledPromise);

      const { headers: cookieHeaders } = cookieStore
      if (cookieHeaders.length) response.headers.append('VARY', 'Cookie')

      const { status, statusText, headers, body } = response
      return new Response(body, {
        status,
        statusText,
        headers: [
          ...headersSetCookieFix(headers),
          ...cookieHeaders,
        ],
      })
    })

    return nx;
  };
}

export const encryptedCookies = (opts: CookiesOptions) => {
  // TODO: options to provide own cryptokey??
  // TODO: What if secret isn't known at initialization (e.g. Cloudflare Workers)
  if (!opts.secret) throw TypeError('Secret missing');

  const keyPromise = EncryptedCookieStore.deriveCryptoKey(opts);

  return async <X extends Context>(ax: Awaitable<X>): Promise<X & EncryptedCookiesContext> => {
    const x = await ax;
    const request = x.request;
    const cookieStore = new RequestCookieStore(request);
    const requestDuration = new ResolvablePromise<void>();
    const encryptedCookieStore = new MiddlewareCookieStore(new EncryptedCookieStore(cookieStore, await keyPromise, {
      keyring: opts.keyring
    }), requestDuration);

    let encryptedCookies: Cookies;
    try {
      encryptedCookies = await cookiesFrom(encryptedCookieStore);
    } catch {
      throw forbidden();
    }

    const nx = Object.assign(x, {
      cookieStore: encryptedCookieStore,
      cookies: encryptedCookies,
      encryptedCookieStore,
      encryptedCookies,
    })

    x.effects.push(async response => {
      // Wait for all set cookie promises to settle
      requestDuration.resolve();
      await unsettle(encryptedCookieStore.allSettledPromise);

      const { headers: cookieHeaders } = cookieStore
      if (cookieHeaders.length) response.headers.append('VARY', 'Cookie')

      const { status, statusText, headers, body } = response
      return new Response(body, {
        status,
        statusText,
        headers: [
          ...headersSetCookieFix(headers),
          ...cookieHeaders,
        ],
      })
    })

    return nx;
  };
}