/**
 * OAuth REST endpoints for SmartAuth.
 *
 * POST /auth/oauth/google     — verify Google id_token, find/create user, auto-link, issue tokens
 * POST /auth/oauth/microsoft  — verify Microsoft id_token (Phase 2B)
 * POST /auth/oauth/apple      — verify Apple id_token (Phase 2B)
 *
 * Account linking (Phase 1B):
 * POST   /auth/providers/link      — link OAuth provider (Bearer)
 * DELETE /auth/providers/:provider  — unlink provider (Bearer)
 * GET    /auth/providers            — list linked providers (Bearer)
 */

import type { FastifyInstance } from 'fastify';
import { BadRequestError, UnauthorizedError, ConflictError } from '../../../lib/errors.js';
import { bus } from '../../../lib/event-bus.js';
import { getProduct } from '../../products/cache.js';
import * as userRepo from '../repository.js';
import * as oauthRepo from './repository.js';
import * as jwt from '../jwt.js';
import { verifyIdToken } from './providers.js';
import { OAuthGoogleSchema, OAuthMicrosoftSchema, OAuthAppleSchema } from './types.js';
import type { UserDoc, AuthProviderDoc } from '../types.js';
import * as subscriptionRepo from '../../subscriptions/repository.js';
import * as licenseRepo from '../../licenses/repository.js';
import * as loginEventRepo from '../login-events/repository.js';
import { scoreLoginRisk } from '../login-events/risk-scorer.js';

export async function oauthRoutes(app: FastifyInstance) {
  // ── Shared OAuth login helper ─────────────────────────────

  async function oauthLoginHelper(
    req: import('fastify').FastifyRequest,
    reply: import('fastify').FastifyReply,
    provider: 'google' | 'microsoft' | 'apple',
    idToken: string,
    productId: string,
    clientDisplayName?: string
  ) {
    const product = getProduct(productId);
    if (!product || product.status !== 'active') {
      throw new BadRequestError(`Unknown or disabled product: ${productId}`);
    }

    const identity = await verifyIdToken(provider, idToken).catch((err: unknown) => {
      const msg = err instanceof Error ? err.message : 'Token verification failed';
      throw new UnauthorizedError(`${provider} OAuth failed: ${msg}`);
    });
    // Override displayName for Apple (only sent on first sign-in)
    if (clientDisplayName) identity.displayName = clientDisplayName;

    const existingProvider = await oauthRepo.getByProviderUserId(provider, identity.providerUserId);
    /* eslint-disable no-useless-assignment */
    let user: UserDoc | null = null;

    let createdNewUser = false;
    /* eslint-enable no-useless-assignment */

    if (existingProvider) {
      user = await userRepo.getById(existingProvider.userId);
      if (!user || user.status !== 'active')
        throw new UnauthorizedError('Account is disabled or not found');
      await oauthRepo.updateLastUsed(user.id, provider);
    } else {
      user = await userRepo.getByEmail(identity.email, productId);
      if (!user) user = await userRepo.getByEmailCrossProduct(identity.email);

      if (user) {
        if (user.status !== 'active') throw new UnauthorizedError('Account is disabled');
      } else {
        createdNewUser = true;
        const now = new Date().toISOString();
        user = {
          id: `usr_${crypto.randomUUID()}`,
          productId,
          email: identity.email,
          passwordHash: await userRepo.hashPassword(crypto.randomUUID()),
          plan: product.defaultPlan,
          role: 'user',
          displayName: identity.displayName || identity.email.split('@')[0],
          status: 'active',
          emailVerified: identity.emailVerified,
          lastLoginAt: now,
          createdAt: now,
          updatedAt: now,
          primaryProductId: productId,
          memberships: [{ productId, plan: product.defaultPlan, role: 'user', firstAccessAt: now }],
        };
        await userRepo.create(user);

        // Best-effort provisioning
        const trialEnd = new Date();
        trialEnd.setDate(trialEnd.getDate() + product.trialDays);
        const hasTrial = product.trialDays > 0;
        try {
          await subscriptionRepo.createSubscription({
            id: `sub_${user.id}_${Date.now()}`,
            productId,
            userId: user.id,
            plan: product.defaultPlan,
            status: hasTrial ? 'trialing' : 'active',
            currentPeriodStart: now,
            currentPeriodEnd: hasTrial ? trialEnd.toISOString() : now,
            cancelAtPeriodEnd: false,
            monthlyPrice: 0,
            tokensIncluded: 0,
            tokensUsed: 0,
            createdAt: now,
            updatedAt: now,
          });
        } catch (err) {
          req.log.warn({ err, userId: user.id }, 'OAuth subscription provisioning failed');
        }
        try {
          await licenseRepo.create({
            id: `lic_${crypto.randomUUID()}`,
            productId,
            key: licenseRepo.generateKey(product.licensePrefix),
            userId: user.id,
            plan: product.defaultPlan,
            status: 'active',
            activatedAt: null,
            expiresAt: hasTrial ? trialEnd.toISOString() : null,
            deviceIds: [],
            maxDevices: product.deviceLimits[product.defaultPlan],
            createdAt: now,
            updatedAt: now,
          });
        } catch (err) {
          req.log.warn({ err, userId: user.id }, 'OAuth license provisioning failed');
        }

        bus
          .emit(
            'user.created',
            { userId: user.id, email: user.email, plan: user.plan, productId },
            { source: `auth/oauth/${provider}` }
          )
          .catch(() => {});
        reply.code(201);
      }

      // Auto-link the identity
      const now = new Date().toISOString();
      const providerDoc: AuthProviderDoc = {
        id: `${user.id}:${provider}`,
        productId: 'smartauth',
        userId: user.id,
        provider,
        providerUserId: identity.providerUserId,
        email: identity.email,
        displayName: identity.displayName,
        linkedAt: now,
        lastUsedAt: now,
      };
      try {
        await oauthRepo.linkProvider(providerDoc);
      } catch {
        /* race condition — ignore */
      }

      bus
        .emit(
          'auth.oauth_linked',
          { userId: user.id, provider, providerEmail: identity.email, productId },
          { source: `auth/oauth/${provider}` }
        )
        .catch(() => {});
    }

    await userRepo.updateLastLogin(user.id);
    const accessToken = await jwt.createAccessToken({
      sub: user.id,
      email: user.email,
      role: user.role,
      productId,
      plan: user.plan,
      displayName: user.displayName,
    });
    const refreshToken = await jwt.createRefreshToken({ sub: user.id, productId });

    // Record OAuth login event (best-effort)
    const ip = req.ip || 'unknown';
    const method = `oauth_${provider}` as 'oauth_google' | 'oauth_microsoft' | 'oauth_apple';
    try {
      const recentFailures = await loginEventRepo.countRecentFailures(user.id, 15 * 60 * 1000);
      const risk = scoreLoginRisk({
        ip,
        isNewIp: true,
        isNewDevice: true,
        isDeviceTrusted: false,
        recentFailures,
        method,
        hourOfDay: new Date().getHours(),
      });
      await loginEventRepo.record({
        id: `le_${crypto.randomUUID()}`,
        userId: user.id,
        productId,
        result: 'success',
        method,
        riskLevel: risk.level,
        riskScore: risk.score,
        ip,
        userAgent: req.headers['user-agent'] as string | undefined,
        riskFlags: risk.flags,
        createdAt: new Date().toISOString(),
      });
    } catch {
      /* best-effort */
    }

    return {
      accessToken,
      refreshToken,
      user: {
        id: user.id,
        email: user.email,
        role: user.role,
        plan: user.plan,
        displayName: user.displayName,
      },
      isNewUser: createdNewUser,
    };
  }

  // ── Google OAuth ────────────────────────────────────────────

  app.post('/auth/oauth/google', async (req, reply) => {
    const parsed = OAuthGoogleSchema.safeParse(req.body);
    if (!parsed.success) {
      throw new BadRequestError(parsed.error.issues.map(i => i.message).join('; '));
    }
    return oauthLoginHelper(req, reply, 'google', parsed.data.idToken, parsed.data.productId);
  });

  // ── Microsoft OAuth (Phase 2B) ──────────────────────────────

  app.post('/auth/oauth/microsoft', async (req, reply) => {
    const parsed = OAuthMicrosoftSchema.safeParse(req.body);
    if (!parsed.success) {
      throw new BadRequestError(parsed.error.issues.map(i => i.message).join('; '));
    }
    return oauthLoginHelper(req, reply, 'microsoft', parsed.data.idToken, parsed.data.productId);
  });

  // ── Apple OAuth (Phase 2B) ─────────────────────────────────

  app.post('/auth/oauth/apple', async (req, reply) => {
    const parsed = OAuthAppleSchema.safeParse(req.body);
    if (!parsed.success) {
      throw new BadRequestError(parsed.error.issues.map(i => i.message).join('; '));
    }
    return oauthLoginHelper(
      req,
      reply,
      'apple',
      parsed.data.idToken,
      parsed.data.productId,
      parsed.data.displayName
    );
  });

  // ── Account Linking (Phase 1B) ─────────────────────────────

  // Link a new OAuth provider to current account
  app.post('/auth/providers/link', async req => {
    const payload = req.jwtPayload;
    if (!payload?.sub) throw new UnauthorizedError('Authentication required');

    const body = req.body as { provider?: string; idToken?: string };
    if (!body.provider || !body.idToken) {
      throw new BadRequestError('provider and idToken are required');
    }
    const provider = body.provider as 'google' | 'microsoft' | 'apple';

    // Verify the id_token
    let identity;
    try {
      identity = await verifyIdToken(provider, body.idToken);
    } catch (err) {
      const msg = err instanceof Error ? err.message : 'Token verification failed';
      throw new UnauthorizedError(`OAuth verification failed: ${msg}`);
    }

    // Check if this provider identity is already linked to another user
    const existingLink = await oauthRepo.getByProviderUserId(provider, identity.providerUserId);
    if (existingLink && existingLink.userId !== payload.sub) {
      throw new ConflictError('This provider account is already linked to another user');
    }
    if (existingLink && existingLink.userId === payload.sub) {
      // Already linked to this user — just update lastUsedAt
      await oauthRepo.updateLastUsed(payload.sub, provider);
      return { message: 'Provider already linked', provider };
    }

    const now = new Date().toISOString();
    const doc: AuthProviderDoc = {
      id: `${payload.sub}:${provider}`,
      productId: 'smartauth',
      userId: payload.sub,
      provider,
      providerUserId: identity.providerUserId,
      email: identity.email,
      displayName: identity.displayName,
      linkedAt: now,
      lastUsedAt: now,
    };
    await oauthRepo.linkProvider(doc);

    bus
      .emit(
        'auth.oauth_linked',
        {
          userId: payload.sub,
          provider,
          providerEmail: identity.email,
          productId: payload.productId || 'smartauth',
        },
        { source: 'auth/providers/link' }
      )
      .catch(() => {});

    req.log.info({ userId: payload.sub, provider }, '[auth] OAuth provider linked');
    return { message: 'Provider linked', provider };
  });

  // Unlink a provider
  app.delete('/auth/providers/:provider', async req => {
    const payload = req.jwtPayload;
    if (!payload?.sub) throw new UnauthorizedError('Authentication required');

    const { provider } = req.params as { provider: string };

    // Must keep at least one auth method
    const user = await userRepo.getById(payload.sub);
    if (!user) throw new UnauthorizedError('User not found');

    const linkedProviders = await oauthRepo.listByUser(payload.sub);
    const hasPassword = !!user.passwordHash && user.passwordHash !== '';
    const otherProviders = linkedProviders.filter(p => p.provider !== provider);

    if (!hasPassword && otherProviders.length === 0) {
      throw new BadRequestError(
        'Cannot unlink last auth method — set a password first or link another provider'
      );
    }

    const deleted = await oauthRepo.unlinkProvider(payload.sub, provider);
    if (!deleted) throw new BadRequestError('Provider not linked');

    bus
      .emit(
        'auth.oauth_unlinked',
        {
          userId: payload.sub,
          provider,
          productId: payload.productId || 'smartauth',
        },
        { source: 'auth/providers/unlink' }
      )
      .catch(() => {});

    req.log.info({ userId: payload.sub, provider }, '[auth] OAuth provider unlinked');
    return { message: 'Provider unlinked', provider };
  });

  // List linked providers
  app.get('/auth/providers', async req => {
    const payload = req.jwtPayload;
    if (!payload?.sub) throw new UnauthorizedError('Authentication required');

    const providers = await oauthRepo.listByUser(payload.sub);
    return {
      providers: providers.map(p => ({
        provider: p.provider,
        email: p.email,
        displayName: p.displayName,
        linkedAt: p.linkedAt,
        lastUsedAt: p.lastUsedAt,
      })),
    };
  });
}
