import { auth } from '@blocklet/sdk/lib/middlewares';
import { getVerifyData, verify } from '@blocklet/sdk/lib/util/verify-sign';
import { verifyLoginToken } from '@blocklet/sdk/lib/util/verify-session';
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) => {
    // Dev-only bypass: requires both NODE_ENV=development AND the explicit
    // opt-in env ENABLE_DEV_FAKE_AUTH=1, plus the x-dev-fake-did header on
    // the request. Lets mobile demo clients that don't go through DID Connect
    // (e.g. the local-tunnel backend which bypasses Blocklet Server) still
    // exercise real handlers. Production never sets ENABLE_DEV_FAKE_AUTH, so
    // this branch can't trigger there; dev defaults off, so we don't
    // accidentally regress to fake auth after wiring the real flow.
    if (process.env.NODE_ENV === 'development' && process.env.ENABLE_DEV_FAKE_AUTH === '1') {
      const devDid = req.get('x-dev-fake-did');
      if (devDid) {
        req.user = {
          did: devDid,
          role: 'owner', // satisfies routes that require owner/admin
          provider: 'dev',
          fullName: 'dev-fake-user',
          walletOS: '',
          via: 'dev',
        };
        return next();
      }
    }

    // Authenticate by Authorization: Bearer <login-token>. The token is a JWT
    // signed with this blocklet's session secret (see @blocklet/sdk session
    // middleware). When clients hit us through a tunnel that bypasses Blocklet
    // Server (so x-user-did is NOT injected), we need to verify the token
    // ourselves. verifyLoginToken does local JWT signature verification, no
    // HTTP callback. On success we forward into the existing x-user-did branch
    // by populating req.headers so the role/mine/record cascade below applies
    // unchanged.
    const authHeader = req.get('authorization');
    if (authHeader && /^Bearer\s+/i.test(authHeader) && !req.headers['x-user-did']) {
      const token = authHeader.replace(/^Bearer\s+/i, '').trim();
      if (token) {
        const session = await verifyLoginToken({ token, strictMode: false }).catch(() => null);
        if (session?.did) {
          // Some BS versions put a bare base58 address in the JWT, others
          // the canonical `did:abt:…` form. Normalize so downstream code
          // (entitlement self-check, mine: lookups by req.user.did) sees the
          // same shape clients send in `customer_did` query params.
          const canonicalDid = session.did.startsWith('did:abt:') ? session.did : `did:abt:${session.did}`;
          req.headers['x-user-did'] = canonicalDid;
          req.headers['x-user-role'] = `blocklet-${session.role || 'user'}`;
          req.headers['x-user-provider'] = session.provider || 'wallet';
          req.headers['x-user-fullname'] = encodeURIComponent(session.fullName || '');
          req.headers['x-user-wallet-os'] = session.walletOS || '';
        }
      }
    }

    // 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' });
  };
}
