import {
  AdminFileToWrite,
  BaseListTypeInfo,
  KeystoneConfig,
  AdminUIConfig,
  BaseKeystoneTypeInfo,
  SessionStrategy,
  KeystoneContext,
} from '@keystone-6/core/types';

import * as cookie from 'cookie';

import type { NextApiRequest } from 'next';

import { Session } from 'next-auth';
import { getSession } from 'next-auth/react';
import { getToken, JWT } from 'next-auth/jwt';
import { Provider } from 'next-auth/providers';

import { nextConfigTemplate } from './templates/next-config';
import { authTemplate } from './templates/auth';

import { getSchemaExtension } from './schema';

import { AuthConfig, KeystoneOAuthConfig, AuthSessionStrategy } from './types';

import url from 'url';

/**
 * createAuth function
 *
 * Generates config for Keystone to implement standard auth features.
 */

export type { 
  NextAuthProviders,
  KeystoneOAuthConfig,
  KeystoneOAuthCallbacks,
  KeystoneOAuthOnSignIn,
  KeystoneOAuthOnSignUp
 } from './types'; 

export function createAuth<GeneratedListTypes extends BaseListTypeInfo>({
  autoCreate,
  context,
  cookies,
  identityField,
  listKey,
  keystonePath,
  onSignIn,
  onSignUp,
  pages,
  providers,
  sessionData,
  sessionSecret,
}: AuthConfig<GeneratedListTypes>) {
  // The protectIdentities flag is currently under review to see whether it should be
  // part of the createAuth API (in which case its use cases need to be documented and tested)
  // or whether always being true is what we want, in which case we can refactor our code
  // to match this. -TL

  const customPath = !keystonePath || keystonePath === '/' ? '' : keystonePath;
  /**
   * pageMiddleware
   *
   * Should be added to the ui.pageMiddleware stack.
   *
   * Redirects:
   *  - from the signin or init pages to the index when a valid session is present
   *  - to the init page when initFirstItem is configured, and there are no user in the database
   *  - to the signin page when no valid session is present
   */
  // TODO: [TYPES] Check pageMiddleware
  const authMiddleware: AdminUIConfig<BaseKeystoneTypeInfo>['pageMiddleware'] =
    async ({ context, isValidSession }) => {
      const { req, session } = context;
      const pathname = url.parse(req?.url!).pathname!;

      if (isValidSession) {
        if (customPath !== '' && pathname === '/') {
          return { kind: 'redirect', to: `${customPath}` };
        }
        return null;
      }
    
      if (!session && !pathname.includes(`${customPath}/api/auth/`)) {
        return {
          kind: 'redirect',
          to: pages?.signIn || `${customPath}/api/auth/signin`,
        };
      }
    };

  /**
   * authGetAdditionalFiles
   *
   * This function adds files to be generated into the Admin UI build. Must be added to the
   * ui.getAdditionalFiles config.
   *
   * The sign-in page is always included, and the init page is included when initFirstItem is set
   */
  const authGetAdditionalFiles = () => {
    const filesToWrite: AdminFileToWrite[] = [
      {
        mode: 'write',
        outputPath: 'pages/api/auth/[...nextauth].js',
        src: authTemplate({
          autoCreate,
          identityField,
          listKey,
          sessionData,
          sessionSecret,
        }),
      },
      {
        mode: 'write',
        outputPath: 'next.config.js',
        src: nextConfigTemplate({ keystonePath: customPath }),
      },
    ];
    return filesToWrite;
  };

  /**
   * publicAuthPages
   *
   * Must be added to the ui.publicPages config
   */
  const authPublicPages = [
    `${customPath}/api/__keystone_api_build`,
    `${customPath}/api/auth/csrf`,
    `${customPath}/api/auth/signin`,
    `${customPath}/api/auth/callback`,
    `${customPath}/api/auth/session`,
    `${customPath}/api/auth/providers`,
    `${customPath}/api/auth/signout`,
    `${customPath}/api/auth/error`,
  ];
  // TODO: [TYPES] Add Provider
  // @ts-ignore
  function addPages(provider: Provider) {
    const name = provider.id;
    authPublicPages.push(`${customPath}/api/auth/signin/${name}`);
    authPublicPages.push(`${customPath}/api/auth/callback/${name}`);
  }
  providers.map(addPages);

  /**
   * extendGraphqlSchema
   *
   * Must be added to the extendGraphqlSchema config. Can be composed.
   */
  const extendGraphqlSchema = getSchemaExtension({
    identityField,
    listKey,
  });

  /**
   * validateConfig
   *
   * Validates the provided auth config; optional step when integrating auth
   */
  const validateConfig = (keystoneConfig: KeystoneConfig) => {
    const listConfig = keystoneConfig.lists[listKey];
    if (listConfig === undefined) {
      const msg = `A createAuth() invocation specifies the list "${listKey}" but no list with that key has been defined.`;
      throw new Error(msg);
    }

    // TODO: Check for String-like typing for identityField? How?
    // TODO: Validate that the identifyField is unique.
    // TODO: If this field isn't required, what happens if I try to log in as `null`?
    const identityFieldConfig = listConfig.fields[identityField];
    if (identityFieldConfig === undefined) {
      const identityFieldName = JSON.stringify(identityField);
      const msg = `A createAuth() invocation for the "${listKey}" list specifies ${identityFieldName} as its identityField but no field with that key exists on the list.`;
      throw new Error(msg);
    }
  };

  /**
   * withItemData
   *
   * Automatically injects a session.data value with the authenticated item
   */
  /* TODO:
  - [ ] We could support additional where input to validate item sessions (e.g an isEnabled boolean)
*/
  const withItemData = (
    _sessionStrategy: AuthSessionStrategy<Record<string, any>>
  ): AuthSessionStrategy<{ listKey: string; itemId: string; data: any }> => {
    const { get, end, ...sessionStrategy } = _sessionStrategy;
    return {
      ...sessionStrategy,
      end: async ({ context }) => {
        await end({ context });
        const TOKEN_NAME =
          process.env.NODE_ENV === 'development'
            ? 'next-auth.session-token'
            : '__Secure-next-auth.session-token';

        const { req, res } = context;
        if (!req || !res) return;

        res.setHeader(
          'Set-Cookie',
          cookie.serialize(TOKEN_NAME, '', {
            // TODO: Update parse to URL
            domain: url.parse(req.url as string).hostname as string,
            expires: new Date(),
            httpOnly: true,
            maxAge: 0,
            path: '/',
            sameSite: 'lax',
            secure: process.env.NODE_ENV === 'production',
          })
        );
      },
      // TODO: [TYPES] Add get typing
      // @ts-ignore
      get: async ({ context }) => {
        const { req } = context;
        const pathname = url.parse(req?.url!).pathname!;
        // TODO 
        let nextSession: JWT | Session & {
          listKey: string;
          itemId: string;
          data: any;
        } | null = null;

        if (!req) return;
        if (pathname.includes('/api/auth')) {
          return null;
        }

        const sudoContext = context.sudo();

        if (req.headers?.authorization?.split(' ')[0] === 'Bearer') {
          nextSession = (await getToken({
            req: req as NextApiRequest,
            secret: sessionSecret,
          }));
        } else {
          nextSession = (await getSession({ req })) as any; // TODO: [TYPES] Review nextSession
        }

        if (
          !nextSession ||
          !nextSession.listKey ||
          nextSession.listKey !== listKey ||
          !nextSession.itemId ||
          !sudoContext.query[listKey] ||
          !nextSession.itemId
        ) {
          return null;
        }
        const reqWithUser = req as any;
        reqWithUser.user = {
          data: nextSession.data,
          itemId: nextSession.itemId,
          listKey: nextSession.listKey,
        };

        const userSession = await get({ context });

        return {
          ...userSession,
          ...nextSession,
          data: nextSession.data,
          itemId: nextSession.itemId,
          listKey: nextSession.listKey,
        };
      },
    };
  };

  function defaultIsAccessAllowed({ session }: KeystoneContext) {
    return session !== undefined;
  }

  /**
   * withAuth
   *
   * Automatically extends config with the correct auth functionality. This is the easiest way to
   * configure auth for keystone; you should probably use it unless you want to extend or replace
   * the way auth is set up with custom functionality.
   *
   * It validates the auth config against the provided keystone config, and preserves existing
   * config by composing existing extendGraphqlSchema functions and ui config.
   */
  const withAuth = (keystoneConfig: KeystoneConfig): KeystoneOAuthConfig => {
    validateConfig(keystoneConfig);
    let { ui } = keystoneConfig;
    
    if (!ui?.isDisabled) {
      const {
        getAdditionalFiles = [],
        isAccessAllowed = defaultIsAccessAllowed,
        pageMiddleware,
        publicPages = [],
      } = ui || {};
      ui = {
        ...ui,
        publicPages: [...publicPages, ...authPublicPages],
        isAccessAllowed: async (context: KeystoneContext) => {
          const pathname = url.parse(context.req?.url!).pathname!;
          if (
            pathname.startsWith(`${customPath}/_next`) ||
            pathname.startsWith(`${customPath}/__next`) ||
            pathname.startsWith(`${customPath}/api/auth/`) ||
            (pages?.signIn && pathname.includes(pages?.signIn)) ||
            (pages?.error && pathname.includes(pages?.error)) ||
            (pages?.signOut && pathname.includes(pages?.signOut))
          ) {
            return true;
          }
          return await isAccessAllowed(context);
        },
        getAdditionalFiles: [...getAdditionalFiles, authGetAdditionalFiles],
        pageMiddleware: async args => {
          if (!authMiddleware) throw new Error('Missing authMiddleware');

          const shouldRedirect = await authMiddleware(args);
          if (shouldRedirect) return shouldRedirect;
          return pageMiddleware?.(args);
        },
      }
    }

    if (!keystoneConfig.session)
      throw new TypeError('Missing .session configuration');
    const session = withItemData(
      keystoneConfig.session
    ) as SessionStrategy<any>;

    const existingExtendGraphQLSchema = keystoneConfig.extendGraphqlSchema;
    return {
      ...keystoneConfig,
      context,
      cookies,
      extendGraphqlSchema: existingExtendGraphQLSchema
        // TODO: [TYPES] Add schema
        ? (schema:any) => existingExtendGraphQLSchema(extendGraphqlSchema(schema))
        : extendGraphqlSchema,
      lists: {
        ...keystoneConfig.lists,
      },
      onSignIn,
      onSignUp,
      pages,
      providers,
      session,
      ui,
    };
  };

  return { withAuth };
}
