import crypto from 'crypto';
import jwt from 'jsonwebtoken';
import { UserRole } from '@prisma/client';
import { prisma } from '../../config/database';
import { env } from '../../config/env';
import { logger } from '../../utils/logger';
import { docsFilesService } from './docs-files.service';
import type { CreateShareLinkInput } from './docs-access.schemas';

// --- Access Policy Resolution ---

interface EffectivePolicy {
  id: string | null;
  documentPath: string;
  isDirectory: boolean;
  allowedEditors: string[];
  isDefault: boolean; // true if no policy found (all content editors)
}

/**
 * Resolve the most specific applicable policy for a document path.
 * Walks from exact path up through parent directories.
 * Returns default "all content editors" if no policy found.
 */
async function getEffectivePolicy(documentPath: string): Promise<EffectivePolicy> {
  // Normalize path: remove leading/trailing slashes
  const normalized = documentPath.replace(/^\/+|\/+$/g, '');

  // 1. Check for exact file match
  const exactPolicy = await prisma.docAccessPolicy.findUnique({
    where: { documentPath: normalized },
  });
  if (exactPolicy) {
    return {
      id: exactPolicy.id,
      documentPath: exactPolicy.documentPath,
      isDirectory: exactPolicy.isDirectory,
      allowedEditors: exactPolicy.allowedEditors as string[],
      isDefault: false,
    };
  }

  // 2. Walk up directory hierarchy looking for directory policies
  const segments = normalized.split('/');
  for (let i = segments.length - 1; i >= 1; i--) {
    const dirPath = segments.slice(0, i).join('/');
    const dirPolicy = await prisma.docAccessPolicy.findUnique({
      where: { documentPath: dirPath },
    });
    if (dirPolicy && dirPolicy.isDirectory) {
      return {
        id: dirPolicy.id,
        documentPath: dirPolicy.documentPath,
        isDirectory: true,
        allowedEditors: dirPolicy.allowedEditors as string[],
        isDefault: false,
      };
    }
  }

  // 3. No policy found — default to all content editors
  return {
    id: null,
    documentPath: normalized,
    isDirectory: false,
    allowedEditors: ['all_content_editors'],
    isDefault: true,
  };
}

/**
 * Check if a user can edit a specific document.
 * SUPER_ADMIN always passes.
 */
async function canUserEdit(
  userId: string,
  userRoles: UserRole[],
  documentPath: string,
): Promise<boolean> {
  // SUPER_ADMIN always passes
  if (userRoles.includes(UserRole.SUPER_ADMIN)) return true;

  const policy = await getEffectivePolicy(documentPath);

  for (const editor of policy.allowedEditors) {
    if (editor === 'all_content_editors') {
      // Check if user has any CONTENT_ROLES
      if (userRoles.includes(UserRole.CONTENT_ADMIN) || userRoles.includes(UserRole.SUPER_ADMIN)) {
        return true;
      }
    } else if (editor.startsWith('user:')) {
      if (editor === `user:${userId}`) return true;
    } else if (editor.startsWith('role:')) {
      const role = editor.substring(5) as UserRole;
      if (userRoles.includes(role)) return true;
    }
  }

  return false;
}

// --- Share Link Management ---

interface ShareLinkOptions {
  canEdit?: boolean;
  expiresInHours?: number;
  maxUses?: number;
  guestName?: string;
}

/**
 * Generate a share link for a document.
 */
async function generateShareLink(
  createdById: string,
  documentPath: string,
  options: ShareLinkOptions = {},
): Promise<{ id: string; shareToken: string; documentPath: string }> {
  // Validate the file exists
  docsFilesService.safeResolve(documentPath);

  const shareToken = crypto.randomBytes(24).toString('hex');
  const expiresAt = options.expiresInHours
    ? new Date(Date.now() + options.expiresInHours * 60 * 60 * 1000)
    : null;

  const link = await prisma.docShareLink.create({
    data: {
      documentPath: documentPath.replace(/^\/+|\/+$/g, ''),
      shareToken,
      canEdit: options.canEdit ?? true,
      expiresAt,
      maxUses: options.maxUses ?? null,
      guestName: options.guestName ?? null,
      createdById,
    },
  });

  return { id: link.id, shareToken: link.shareToken, documentPath: link.documentPath };
}

/**
 * Validate a share token. Returns the share link or throws.
 * Increments use count on successful validation.
 */
async function validateShareToken(shareToken: string): Promise<{
  id: string;
  documentPath: string;
  canEdit: boolean;
  guestName: string | null;
}> {
  const link = await prisma.docShareLink.findUnique({
    where: { shareToken },
  });

  if (!link) throw new ShareLinkError('Share link not found', 'SHARE_LINK_NOT_FOUND');
  if (link.status !== 'ACTIVE') throw new ShareLinkError('Share link has been revoked', 'SHARE_LINK_REVOKED');
  if (link.expiresAt && link.expiresAt < new Date()) {
    // Auto-expire
    await prisma.docShareLink.update({
      where: { id: link.id },
      data: { status: 'EXPIRED' },
    });
    throw new ShareLinkError('Share link has expired', 'SHARE_LINK_EXPIRED');
  }
  if (link.maxUses && link.useCount >= link.maxUses) {
    throw new ShareLinkError('Share link has reached maximum uses', 'SHARE_LINK_MAX_USES');
  }

  // Increment use count
  await prisma.docShareLink.update({
    where: { id: link.id },
    data: { useCount: { increment: 1 } },
  });

  return {
    id: link.id,
    documentPath: link.documentPath,
    canEdit: link.canEdit,
    guestName: link.guestName,
  };
}

/**
 * Generate a short-lived JWT for a share-link guest to use with the collab WebSocket.
 */
function generateShareCollabToken(shareLink: {
  shareToken: string;
  documentPath: string;
  canEdit: boolean;
  guestName: string | null;
}): string {
  return jwt.sign(
    {
      type: 'doc_share',
      shareToken: shareLink.shareToken,
      documentPath: shareLink.documentPath,
      canEdit: shareLink.canEdit,
      guestName: shareLink.guestName || 'Guest',
    },
    env.JWT_INVITE_SECRET,
    { expiresIn: '4h' },
  );
}

// --- Cascade Operations ---

/**
 * Update access policies and share links when a file is renamed.
 */
async function cascadeRename(oldPath: string, newPath: string): Promise<void> {
  const normalizedOld = oldPath.replace(/^\/+|\/+$/g, '');
  const normalizedNew = newPath.replace(/^\/+|\/+$/g, '');

  try {
    // Update exact match policy
    await prisma.docAccessPolicy.updateMany({
      where: { documentPath: normalizedOld },
      data: { documentPath: normalizedNew },
    });

    // Update share links
    await prisma.docShareLink.updateMany({
      where: { documentPath: normalizedOld },
      data: { documentPath: normalizedNew },
    });

    // Update doc watches
    await prisma.docWatch.updateMany({
      where: { filePath: normalizedOld },
      data: { filePath: normalizedNew },
    });

    logger.info(`Docs access: cascaded rename ${normalizedOld} → ${normalizedNew}`);
  } catch (err) {
    logger.warn('Failed to cascade rename for docs access:', err);
  }
}

/**
 * Revoke share links and delete policy when a file is deleted.
 */
async function cascadeDelete(path: string): Promise<void> {
  const normalized = path.replace(/^\/+|\/+$/g, '');

  try {
    // Revoke active share links
    await prisma.docShareLink.updateMany({
      where: { documentPath: normalized, status: 'ACTIVE' },
      data: { status: 'REVOKED' },
    });

    // Delete access policy
    await prisma.docAccessPolicy.deleteMany({
      where: { documentPath: normalized },
    });

    // Delete watches
    await prisma.docWatch.deleteMany({
      where: { filePath: normalized },
    });

    logger.info(`Docs access: cascaded delete for ${normalized}`);
  } catch (err) {
    logger.warn('Failed to cascade delete for docs access:', err);
  }
}

export class ShareLinkError extends Error {
  code: string;
  constructor(message: string, code: string) {
    super(message);
    this.name = 'ShareLinkError';
    this.code = code;
  }
}

export const docsAccessService = {
  getEffectivePolicy,
  canUserEdit,
  generateShareLink,
  validateShareToken,
  generateShareCollabToken,
  cascadeRename,
  cascadeDelete,
};
