import { createHmac } from 'crypto';
import { Prisma, UserRole } from '@prisma/client';
import { prisma } from '../../config/database';
import { env } from '../../config/env';
import { logger } from '../../utils/logger';
import { giteaClient } from '../gitea.client';
import { CONTENT_ROLES } from '../../utils/roles';
import type { ServiceProvisioner, ProvisionerConfig, ProvisionResult, CMUser } from './provisioner.interface';

const ROLE_MAP: Record<string, string[]> = {
  SUPER_ADMIN: ['admin'],
  INFLUENCE_ADMIN: ['user'],
  MAP_ADMIN: ['user'],
  USER: ['user'],
  TEMP: [],
};

/** The private docs repo name created by gitea-setup */
const DOCS_REPO_NAME = 'changemaker.lite';

/** Deterministic password — never exposed to users */
function generateGiteaPassword(userId: string): string {
  const salt = env.SERVICE_PASSWORD_SALT;
  return createHmac('sha256', salt)
    .update(`gitea:${userId}`)
    .digest('hex');
}

/** Safe username from email — Gitea allows alphanumeric, dash, underscore, max 40 chars */
function generateUsername(email: string, suffix = 0): string {
  const base = email.split('@')[0].toLowerCase().replace(/[^a-z0-9_-]/g, '').slice(0, 36);
  return suffix > 0 ? `${base}${suffix}` : base;
}

class GiteaProvisioner implements ServiceProvisioner {
  readonly config: ProvisionerConfig = {
    serviceKey: 'gitea',
    displayName: 'Gitea',
    featureFlag: 'provisionGitea',
    permissionsKey: '_giteaUserId',
    roleMap: ROLE_MAP,
    excludeRoles: ['TEMP'],
  };

  async isAvailable(): Promise<boolean> {
    return giteaClient.isAvailable();
  }

  async provision(user: CMUser): Promise<ProvisionResult> {
    try {
      // Check if already exists in Gitea
      let giteaUser = await giteaClient.findUserByEmail(user.email);

      if (!giteaUser) {
        // Create with collision-safe username
        let username = generateUsername(user.email);
        let suffix = 0;
        const maxAttempts = 5;

        while (suffix < maxAttempts) {
          const existing = await giteaClient.getUserByUsername(username);
          if (!existing) break;
          suffix++;
          username = generateUsername(user.email, suffix);
        }

        const isAdmin = user.role === 'SUPER_ADMIN';

        giteaUser = await giteaClient.adminCreateUser({
          email: user.email,
          full_name: user.name || user.email.split('@')[0],
          login_name: username,
          username,
          password: generateGiteaPassword(user.id),
          must_change_password: false,
          send_notify: false,
        });

        // Set admin status if needed (separate call for clarity)
        if (isAdmin) {
          await giteaClient.adminUpdateUser(username, { admin: true }).catch(err => {
            logger.warn(`Gitea provisioner: failed to set admin for ${username}:`, err);
          });
        }

        logger.info(`Gitea provisioner: created user ${giteaUser.login} (id=${giteaUser.id}) for ${user.email}`);
      }

      // Persist Gitea user info in permissions
      const permissions = (user.permissions as Record<string, unknown>) || {};
      await prisma.user.update({
        where: { id: user.id },
        data: {
          permissions: {
            ...permissions,
            [this.config.permissionsKey]: giteaUser.id,
            _giteaUsername: giteaUser.login,
          } as unknown as Prisma.InputJsonValue,
        },
      });

      // Grant docs repo access based on role
      await this.syncDocsRepoAccess(giteaUser.login, user.role);

      return {
        success: true,
        serviceUserId: String(giteaUser.id),
        extraPermissions: { _giteaUsername: giteaUser.login },
      };
    } catch (err) {
      const msg = err instanceof Error ? err.message : String(err);
      logger.error(`Gitea provisioner: provision failed for ${user.email}: ${msg}`);
      return { success: false, error: msg };
    }
  }

  async syncUser(user: CMUser, serviceUserId: string): Promise<void> {
    // Look up username from permissions
    const permissions = (user.permissions as Record<string, unknown>) || {};
    const username = permissions._giteaUsername as string;
    if (!username) {
      logger.warn(`Gitea provisioner: no username cached for user ${user.id}, skipping sync`);
      return;
    }

    const isAdmin = user.role === 'SUPER_ADMIN';
    await giteaClient.adminUpdateUser(username, {
      full_name: user.name || user.email.split('@')[0],
      admin: isAdmin,
      active: user.status === 'ACTIVE',
    });

    // Re-evaluate docs repo access based on current role
    await this.syncDocsRepoAccess(username, user.role);
  }

  async deactivate(serviceUserId: string): Promise<void> {
    // We need the username to deactivate — look it up from any user with this service ID
    const user = await prisma.user.findFirst({
      where: {
        permissions: { path: ['_giteaUserId'], equals: parseInt(serviceUserId, 10) },
      },
      select: { permissions: true },
    });

    const permissions = (user?.permissions as Record<string, unknown>) || {};
    const username = permissions._giteaUsername as string;
    if (!username) {
      logger.warn(`Gitea provisioner: cannot find username for serviceUserId ${serviceUserId}`);
      return;
    }

    await giteaClient.adminUpdateUser(username, { active: false });

    // Remove docs repo collaborator access
    try {
      const config = await giteaClient.getConfig();
      if (config.repoOwner) {
        await giteaClient.removeCollaborator(config.repoOwner, DOCS_REPO_NAME, username);
      }
    } catch {
      // Ignore — user may not have been a collaborator
    }

    logger.info(`Gitea provisioner: deactivated user ${username}`);
  }

  async getAuthToken(_user: CMUser, _serviceUserId: string): Promise<string | null> {
    return null;
  }

  /**
   * Ensure the user's collaborator status on the private docs repo matches
   * their role. CONTENT_ROLES (SUPER_ADMIN, CONTENT_ADMIN) get write access;
   * SUPER_ADMIN users already have admin access via the Gitea admin flag,
   * but we also add them as explicit collaborators for consistency.
   * Users without CONTENT_ROLES are removed as collaborators.
   */
  private async syncDocsRepoAccess(username: string, role: string): Promise<void> {
    try {
      const config = await giteaClient.getConfig();
      const repoOwner = config.repoOwner;
      if (!repoOwner) return; // Setup not complete — no repo owner yet

      const hasDocsAccess = CONTENT_ROLES.includes(role as UserRole);

      if (hasDocsAccess) {
        // SUPER_ADMIN → admin, CONTENT_ADMIN → write
        const permission = role === 'SUPER_ADMIN' ? 'admin' : 'write';
        await giteaClient.addCollaborator(repoOwner, DOCS_REPO_NAME, username, permission);
        logger.debug(`Gitea provisioner: granted ${permission} access to ${repoOwner}/${DOCS_REPO_NAME} for ${username}`);
      } else {
        // Remove access if user no longer has CONTENT_ROLES
        const isCollab = await giteaClient.isCollaborator(repoOwner, DOCS_REPO_NAME, username);
        if (isCollab) {
          await giteaClient.removeCollaborator(repoOwner, DOCS_REPO_NAME, username);
          logger.debug(`Gitea provisioner: removed ${username} from ${repoOwner}/${DOCS_REPO_NAME}`);
        }
      }
    } catch (err) {
      // Non-fatal — don't block provisioning if repo access sync fails
      logger.warn(`Gitea provisioner: docs repo access sync failed for ${username}:`, err instanceof Error ? err.message : err);
    }
  }
}

export const giteaProvisioner = new GiteaProvisioner();
