/**
 * JWT Auth Plugin
 * 
 * Provides M2M (Machine-to-Machine) authentication via JWT tokens.
 * Validates JWT tokens using JWKS.
 */

import * as jose from 'jose';
import { SyngrisiPlugin, PluginContext, AuthResult } from '../../sdk/types';
import { registerPluginSchema } from '../../../controllers/plugin-settings.controller';

interface JwtAuthConfig {
    /** JWKS endpoint URL for token validation */
    jwksUrl: string;

    /** Expected JWT issuer */
    issuer: string;

    /** Header name containing the JWT token */
    headerName: string;

    /** Header value prefix (e.g. "Bearer ") */
    headerPrefix: string;

    /** Role to assign to service users */
    serviceUserRole: 'user' | 'reviewer' | 'admin';

    /** Whether to auto-provision service users in DB */
    autoProvisionUsers: boolean;

    /** Cache TTL for JWKS in milliseconds */
    jwksCacheTtl: number;

    /** Optional audience(s) to validate */
    audience?: string[] | string;

    /** Optional required scopes */
    requiredScopes?: string[] | string;

    /** Issuer matching mode: strict (default) or host */
    issuerMatch?: 'strict' | 'host';
}

const DEFAULT_CONFIG: JwtAuthConfig = {
    jwksUrl: '',
    issuer: '',
    headerName: 'Authorization',
    headerPrefix: 'Bearer ',
    serviceUserRole: 'user',
    autoProvisionUsers: true,
    jwksCacheTtl: 3600000, // 1 hour
    issuerMatch: 'strict',
};

const SETTINGS_SCHEMA = [
    {
        key: 'jwksUrl',
        label: 'JWKS URL',
        description: 'URL to the JWKS (JSON Web Key Set) endpoint for token validation',
        type: 'string' as const,
        envVariable: 'SYNGRISI_PLUGIN_JWT_AUTH_JWKS_URL',
        required: true,
    },
    {
        key: 'issuer',
        label: 'JWT Issuer',
        description: 'Expected issuer claim in the JWT token',
        type: 'string' as const,
        envVariable: 'SYNGRISI_PLUGIN_JWT_AUTH_ISSUER',
        required: true,
    },
    {
        key: 'headerName',
        label: 'Header Name',
        description: 'HTTP header name containing the token (e.g. Authorization)',
        type: 'string' as const,
        defaultValue: 'Authorization',
        envVariable: 'SYNGRISI_PLUGIN_JWT_AUTH_HEADER_NAME',
    },
    {
        key: 'headerPrefix',
        label: 'Header Prefix',
        description: 'Prefix to strip from header value (e.g. "Bearer "). Leave empty if none.',
        type: 'string' as const,
        defaultValue: 'Bearer ',
        envVariable: 'SYNGRISI_PLUGIN_JWT_AUTH_HEADER_PREFIX',
    },
    {
        key: 'serviceUserRole',
        label: 'Service User Role',
        description: 'Role to assign to auto-provisioned service users',
        type: 'select' as const,
        defaultValue: 'user',
        envVariable: 'SYNGRISI_PLUGIN_JWT_AUTH_SERVICE_USER_ROLE',
        options: [
            { value: 'user', label: 'User' },
            { value: 'reviewer', label: 'Reviewer' },
            { value: 'admin', label: 'Admin' },
        ],
    },
    {
        key: 'autoProvisionUsers',
        label: 'Auto-provision Users',
        description: 'Automatically create service users in database for new clients',
        type: 'boolean' as const,
        defaultValue: true,
        envVariable: 'SYNGRISI_PLUGIN_JWT_AUTH_AUTO_PROVISION',
    },
    {
        key: 'jwksCacheTtl',
        label: 'JWKS Cache TTL (ms)',
        description: 'Cache duration for JWKS keys in milliseconds',
        type: 'number' as const,
        defaultValue: 3600000,
        envVariable: 'SYNGRISI_PLUGIN_JWT_AUTH_JWKS_CACHE_TTL',
    },
    {
        key: 'audience',
        label: 'Audience',
        description: 'Expected audience claim(s). Comma- or space-separated.',
        type: 'string' as const,
        envVariable: 'SYNGRISI_PLUGIN_JWT_AUTH_AUDIENCE',
    },
    {
        key: 'requiredScopes',
        label: 'Required Scopes',
        description: 'Required scopes. Comma- or space-separated.',
        type: 'string' as const,
        envVariable: 'SYNGRISI_PLUGIN_JWT_AUTH_REQUIRED_SCOPES',
    },
    {
        key: 'issuerMatch',
        label: 'Issuer Match Mode',
        description: 'Strict requires exact issuer match. Host allows matching by hostname.',
        type: 'select' as const,
        defaultValue: 'strict',
        envVariable: 'SYNGRISI_PLUGIN_JWT_AUTH_ISSUER_MATCH',
        options: [
            { value: 'strict', label: 'Strict' },
            { value: 'host', label: 'Host' },
        ],
    },
];

const parseList = (value?: string | string[]): string[] => {
    if (!value) return [];
    if (Array.isArray(value)) {
        return value.map(v => String(v).trim()).filter(Boolean);
    }
    return value
        .split(/[\s,]+/)
        .map(v => v.trim())
        .filter(Boolean);
};

const extractHost = (issuer: string): string | null => {
    try {
        const parsed = new URL(issuer);
        return parsed.host;
    } catch {
        return null;
    }
};

/**
 * Create JWT Auth Plugin
 */
export function createJwtAuthPlugin(initialConfig: Partial<JwtAuthConfig> = {}): SyngrisiPlugin {
    let config: JwtAuthConfig = { ...DEFAULT_CONFIG, ...initialConfig };
    let jwks: jose.JWTVerifyGetKey | null = null;
    let jwksInitialized = false;

    return {
        manifest: {
            name: 'jwt-auth',
            version: '1.0.0',
            description: 'M2M authentication via JWT (OAuth2/OIDC)',
            author: 'Syngrisi Team',
            priority: 10, // High priority - runs before standard auth
        },

        async onLoad(context: PluginContext): Promise<void> {
            const logger = context.logger;
            const logOpts = { scope: 'jwt-auth', msgType: 'PLUGIN' };

            // Merge config from plugin context (DB settings have priority)
            config = { ...config, ...context.pluginConfig as Partial<JwtAuthConfig> };

            // Register settings schema for UI
            await registerPluginSchema(
                'jwt-auth',
                'JWT Authentication',
                'M2M authentication via JWT (OAuth2 Client Credentials)',
                SETTINGS_SCHEMA
            );

            // Validate required config - throw errors to prevent silent failures
            if (!config.jwksUrl) {
                const errMsg =
                    'Missing required configuration for "jwt-auth" plugin. ' +
                    'SYNGRISI_PLUGIN_JWT_AUTH_JWKS_URL is required. ' +
                    'Set SYNGRISI_PLUGIN_JWT_AUTH_JWKS_URL environment variable or configure via Admin UI.';
                console.error(errMsg);
                throw new Error(errMsg);
            }

            if (!config.issuer) {
                const errMsg =
                    'Missing required configuration for "jwt-auth" plugin. ' +
                    'SYNGRISI_PLUGIN_JWT_AUTH_ISSUER is required. ' +
                    'Set SYNGRISI_PLUGIN_JWT_AUTH_ISSUER environment variable or configure via Admin UI.';
                console.error(errMsg);
                throw new Error(errMsg);
            }

            try {
                new URL(config.jwksUrl);
            } catch {
                const errMsg = 'Invalid JWKS URL';
                console.error(errMsg);
                throw new Error(errMsg);
            }

            // Initialize JWKS client
            try {
                jwks = jose.createRemoteJWKSet(new URL(config.jwksUrl), {
                    cacheMaxAge: config.jwksCacheTtl,
                });
                jwksInitialized = true;
                logger.info(`JWT Auth plugin loaded. JWKS: ${config.jwksUrl}`, logOpts);
            } catch (error) {
                const errMsg = `JWT Auth: Failed to initialize JWKS client: ${error}`;
                console.error(errMsg);
                throw new Error(errMsg);
            }
        },

        async onUnload(): Promise<void> {
            jwks = null;
            jwksInitialized = false;
        },

        hooks: {
            'auth:validate': async (req, res, context): Promise<AuthResult | null> => {
                const logger = context.logger;
                const logOpts = { scope: 'jwt-auth', msgType: 'AUTH' };

                // Skip if not properly initialized
                if (!jwksInitialized || !jwks) {
                    return null;
                }

                // Get token from header
                const headerName = config.headerName.toLowerCase();
                const authHeader = req.headers[headerName];

                if (!authHeader || typeof authHeader !== 'string') {
                    return null;
                }

                // Extract token (remove prefix)
                let token = authHeader;
                if (config.headerPrefix) {
                    const prefix = config.headerPrefix;
                    if (token.startsWith(prefix)) {
                        token = token.slice(prefix.length);
                    } else if (token.toLowerCase().startsWith(prefix.toLowerCase())) {
                        token = token.slice(prefix.length);
                    }
                }
                token = token.trim();

                if (!token) {
                    return {
                        authenticated: false,
                        error: 'Missing token in authorization header',
                    };
                }

                try {
                    const expectedIssuers = parseList(config.issuer);
                    const audiences = parseList(config.audience);
                    const requiredScopes = parseList(config.requiredScopes);

                    // Validate JWT signature and claims
                    const verifyOptions: jose.JWTVerifyOptions = {};
                    if (config.issuerMatch !== 'host') {
                        verifyOptions.issuer = expectedIssuers.length > 1 ? expectedIssuers : expectedIssuers[0];
                    }
                    if (audiences.length > 0) {
                        verifyOptions.audience = audiences.length > 1 ? audiences : audiences[0];
                    }

                    const { payload } = await jose.jwtVerify(token, jwks, verifyOptions);

                    if (config.issuerMatch === 'host') {
                        const tokenIssuer = payload.iss as string | undefined;
                        if (!tokenIssuer) {
                            return {
                                authenticated: false,
                                error: 'Token is missing issuer claim',
                            };
                        }
                        const tokenHost = extractHost(tokenIssuer);
                        const expectedHosts = expectedIssuers
                            .map(issuer => extractHost(issuer) ?? issuer)
                            .filter(Boolean);
                        const matchesHost = expectedHosts.some(host => tokenHost ? tokenHost === host : tokenIssuer === host);
                        if (!matchesHost) {
                            return {
                                authenticated: false,
                                error: 'Token issuer does not match expected host',
                            };
                        }
                    }

                    const clientId =
                        (payload.sub as string | undefined) ||
                        (payload.client_id as string | undefined) ||
                        (payload.cid as string | undefined);

                    if (!clientId) {
                        logger.warn('JWT Auth: missing client identifier in token (sub/client_id/cid)', logOpts);
                        return {
                            authenticated: false,
                            error: 'Token is missing client identifier',
                        };
                    }
                    const scp = (payload as Record<string, unknown>).scp;
                    const scope = (payload as Record<string, unknown>).scope;
                    let scopes: string[] = [];

                    if (Array.isArray(scp)) {
                        scopes = scp.map(value => String(value));
                    } else if (typeof scp === 'string') {
                        scopes = scp.split(' ');
                    } else if (Array.isArray(scope)) {
                        scopes = scope.map(value => String(value));
                    } else if (typeof scope === 'string') {
                        scopes = scope.split(' ');
                    }

                    scopes = scopes.map(value => value.trim()).filter(Boolean);

                    if (requiredScopes.length > 0) {
                        const scopeSet = new Set(scopes.map(scope => String(scope).trim()).filter(Boolean));
                        const missing = requiredScopes.filter(scope => !scopeSet.has(scope));
                        if (missing.length > 0) {
                            return {
                                authenticated: false,
                                error: `Token missing required scopes: ${missing.join(', ')}`,
                            };
                        }
                    }

                    logger.info(`JWT Auth: validated token for client ${clientId}`, logOpts);

                    const { User } = context.models;
                    const username = `jwt-service:${clientId}`;

                    // Find or create service user (depending on autoProvisionUsers setting)
                    let serviceUser = await User.findOne({ username });

                    if (!serviceUser) {
                        if (config.autoProvisionUsers) {
                            // Auto-provision new service user
                            serviceUser = await User.create({
                                username,
                                firstName: 'JWT',
                                lastName: `Service (${clientId.substring(0, 8)}...)`,
                                role: config.serviceUserRole,
                                authSource: 'jwt',
                            });
                            logger.info(`Created service user: ${username}`, logOpts);
                        } else {
                            // Auto-provision disabled - reject authentication
                            logger.warn(`JWT Auth: User not found and auto-provision is disabled: ${username}`, logOpts);
                            return {
                                authenticated: false,
                                error: `Service user not found: ${clientId}. Auto-provisioning is disabled.`,
                            };
                        }
                    }

                    return {
                        authenticated: true,
                        user: serviceUser,
                        metadata: {
                            clientId,
                            scopes,
                            authMethod: 'jwt',
                        },
                    };

                } catch (error) {
                    if (error instanceof jose.errors.JWTExpired) {
                        logger.warn(`JWT Auth: token expired`, logOpts);
                        return {
                            authenticated: false,
                            error: 'Token expired',
                        };
                    }

                    if (error instanceof jose.errors.JWTClaimValidationFailed) {
                        logger.warn(`JWT Auth: claim validation failed: ${error.message}`, logOpts);
                        return {
                            authenticated: false,
                            error: `Token validation failed: ${error.message}`,
                        };
                    }

                    logger.error(`JWT Auth: validation error: ${error}`, logOpts);
                    return {
                        authenticated: false,
                        error: 'Token validation failed',
                    };
                }
            },
        },
    };
}

// Default export for plugin loader
export default createJwtAuthPlugin;
