import { auth } from '@blocklet/sdk/lib/middlewares';
import { getVerifyData, verify } from '@blocklet/sdk/lib/util/verify-sign';
import { getWallet } from '@blocklet/sdk/lib/wallet';
import type { NextFunction, Request, Response } from 'express';
import type { Model } from 'sequelize';

import { Customer } from '../store/models/customer';

export const ensureAdmin = auth({ roles: ['owner', 'admin'] });

const wallet = getWallet();

type PermissionSpec<T extends Model> = {
  component?: boolean; // allow component calls
  roles?: string[]; // allow current session user with one of the specified roles
  record?: {
    // allow record owner
    model: T;
    field: string;
    findById?: (id: string) => Promise<T | null>;
  };
  mine?: boolean;
  embed?: boolean;
  ensureLogin?: boolean;
};

/**
 * This middleware is used to authenticate request by session or component call.
 * If a request is authenticated, it will set `req.user` to the session user.
 * If a request is authenticated by component call, it will set `req.user` to the component user.
 * If a request is authenticated by record owner, it will set `req.user` to the session user and set `req.doc` to the record.
 */
export function authenticate<T extends Model>({
  component,
  roles,
  record,
  mine,
  embed,
  ensureLogin,
}: PermissionSpec<T>) {
  return async (req: Request, res: Response, next: NextFunction) => {
    // authenticate by component call
    const sig = req.get('x-component-sig');
    if (component && sig) {
      const { data } = getVerifyData(req as any, 'component');
      const verified = await verify(data, sig);
      if (!verified) {
        return res.status(401).json({ error: 'Invalid signature for component call' });
      }

      req.user = {
        did: <string>req.get('x-component-did'),
        role: 'owner',
        provider: 'wallet',
        fullName: <string>req.get('x-component-did'),
        walletOS: '',
        via: 'api',
      };

      return next();
    }

    // authenticate by authToken for embed
    const token = req.query.authToken || '';
    const id = req.params.id || req.query.subscription_id || '';
    if (embed && token && id) {
      try {
        const verified = await wallet.verify(id as string, token as string);
        if (!verified) {
          return res.status(401).json({ error: `Invalid signature for embed: ${id}` });
        }

        req.user = {
          did: wallet.address,
          role: 'owner',
          provider: 'wallet',
          fullName: 'embed',
          walletOS: '',
          via: 'embed',
        };

        return next();
      } catch (err) {
        return res.status(401).json({ error: `Invalid signature for embed: ${id}: ${err.message}` });
      }
    }

    if (req.headers['x-user-did']) {
      const role = (<string>req.headers['x-user-role'] || '').replace('blocklet-', '') || 'guest';
      req.user = {
        did: <string>req.headers['x-user-did'],
        role,
        provider: <string>req.headers['x-user-provider'],
        fullName: decodeURIComponent(<string>req.headers['x-user-fullname']),
        walletOS: <string>req.headers['x-user-wallet-os'],
        via: 'dashboard',
      };

      // authenticate by session user
      if (roles) {
        if (roles.includes(req.user?.role)) {
          return next();
        }
      }

      if (ensureLogin) {
        req.user.via = 'api';
        return next();
      }

      if (mine) {
        const customer = await Customer.findOne({ where: { did: req.user.did } });
        if (customer) {
          req.customer = customer;
          req.query.customer_id = customer.id;
          return next();
        }
      }

      // authenticate by record owner
      if (record) {
        const { model, field = 'customer_id', findById } = record;
        const doc: T | null =
          findById && typeof findById === 'function'
            ? await findById(req.params.id as string)
            : await (model as any).findByPk(req.params.id);
        if (doc && doc[field as keyof T]) {
          const customer = await Customer.findOne({ where: { did: req.user.did } });
          req.doc = doc;
          req.customer = customer;
          if (customer && customer.id === doc[field as keyof T]) {
            req.user.via = 'portal';
            return next();
          }
        }
      }
    }

    return res.status(403).json({ error: 'Not authorized to perform this action' });
  };
}
