import crypto from 'crypto';
import { db } from '../db';
import { apiKeys, organizations, projects, type ApiKey } from '@banatie/database';
import { eq, and, desc } from 'drizzle-orm';

// Extended API key type with slugs for storage paths
export interface ApiKeyWithSlugs extends ApiKey {
  organizationSlug?: string;
  projectSlug?: string;
}

// Extended API key type with full organization and project details for admin listing
export interface ApiKeyWithDetails extends ApiKey {
  organization: {
    id: string;
    slug: string;
    name: string;
    email: string;
  } | null;
  project: {
    id: string;
    slug: string;
    name: string;
  } | null;
}

export class ApiKeyService {
  /**
   * Generate a new API key
   * Format: bnt_{64_hex_chars}
   */
  private generateKey(): {
    fullKey: string;
    keyHash: string;
    keyPrefix: string;
  } {
    const secret = crypto.randomBytes(32).toString('hex'); // 64 chars
    const keyPrefix = 'bnt_';
    const fullKey = keyPrefix + secret;

    // Hash for storage (SHA-256)
    const keyHash = crypto.createHash('sha256').update(fullKey).digest('hex');

    return { fullKey, keyHash, keyPrefix };
  }

  /**
   * Create a master key (admin access, never expires)
   */
  async createMasterKey(
    name?: string,
    createdBy?: string,
  ): Promise<{ key: string; metadata: ApiKey }> {
    const { fullKey, keyHash, keyPrefix } = this.generateKey();

    const [newKey] = await db
      .insert(apiKeys)
      .values({
        keyHash,
        keyPrefix,
        keyType: 'master',
        projectId: null,
        scopes: ['*'], // Full access
        name: name || 'Master Key',
        expiresAt: null, // Never expires
        createdBy: createdBy || null,
      })
      .returning();

    console.log(
      `[${new Date().toISOString()}] Master key created: ${newKey?.id} - ${newKey?.name}`,
    );

    return { key: fullKey, metadata: newKey! };
  }

  /**
   * Create a project key (expires in 90 days)
   */
  async createProjectKey(
    projectId: string,
    organizationId?: string,
    name?: string,
    createdBy?: string,
    expiresInDays: number = 90,
  ): Promise<{ key: string; metadata: ApiKey }> {
    const { fullKey, keyHash, keyPrefix } = this.generateKey();

    const expiresAt = new Date();
    expiresAt.setDate(expiresAt.getDate() + expiresInDays);

    const [newKey] = await db
      .insert(apiKeys)
      .values({
        keyHash,
        keyPrefix,
        keyType: 'project',
        projectId,
        organizationId: organizationId || null,
        scopes: ['generate', 'read'],
        name: name || `Project Key - ${projectId}`,
        expiresAt,
        createdBy: createdBy || null,
      })
      .returning();

    console.log(
      `[${new Date().toISOString()}] Project key created: ${newKey?.id} - ${projectId} - expires: ${expiresAt.toISOString()}`,
    );

    return { key: fullKey, metadata: newKey! };
  }

  /**
   * Validate an API key
   * Returns null if invalid/expired/revoked
   * Returns API key with organization and project slugs for storage paths
   */
  async validateKey(providedKey: string): Promise<ApiKeyWithSlugs | null> {
    if (!providedKey || !providedKey.startsWith('bnt_')) {
      return null;
    }

    // Hash the provided key
    const keyHash = crypto.createHash('sha256').update(providedKey).digest('hex');

    // Find in database with left joins to get slugs
    const [result] = await db
      .select({
        // API key fields
        id: apiKeys.id,
        keyHash: apiKeys.keyHash,
        keyPrefix: apiKeys.keyPrefix,
        keyType: apiKeys.keyType,
        organizationId: apiKeys.organizationId,
        projectId: apiKeys.projectId,
        scopes: apiKeys.scopes,
        createdAt: apiKeys.createdAt,
        expiresAt: apiKeys.expiresAt,
        lastUsedAt: apiKeys.lastUsedAt,
        isActive: apiKeys.isActive,
        name: apiKeys.name,
        createdBy: apiKeys.createdBy,
        // Slug fields
        organizationSlug: organizations.slug,
        projectSlug: projects.slug,
      })
      .from(apiKeys)
      .leftJoin(organizations, eq(apiKeys.organizationId, organizations.id))
      .leftJoin(projects, eq(apiKeys.projectId, projects.id))
      .where(and(eq(apiKeys.keyHash, keyHash), eq(apiKeys.isActive, true)))
      .limit(1);

    if (!result) {
      console.warn(
        `[${new Date().toISOString()}] Invalid API key attempt: ${providedKey.substring(0, 10)}...`,
      );
      return null;
    }

    // Check expiration
    if (result.expiresAt && result.expiresAt < new Date()) {
      console.warn(
        `[${new Date().toISOString()}] Expired API key used: ${result.id} - expired: ${result.expiresAt.toISOString()}`,
      );
      return null;
    }

    // Update last used timestamp (async, don't wait)
    db.update(apiKeys)
      .set({ lastUsedAt: new Date() })
      .where(eq(apiKeys.id, result.id))
      .execute()
      .catch((err) =>
        console.error(`[${new Date().toISOString()}] Failed to update lastUsedAt:`, err),
      );

    return result as ApiKeyWithSlugs;
  }

  /**
   * Revoke a key (soft delete)
   */
  async revokeKey(keyId: string): Promise<boolean> {
    const result = await db
      .update(apiKeys)
      .set({ isActive: false })
      .where(eq(apiKeys.id, keyId))
      .returning();

    if (result.length > 0) {
      console.log(`[${new Date().toISOString()}] API key revoked: ${keyId}`);
      return true;
    }

    return false;
  }

  /**
   * List all keys (for admin) with organization and project details
   */
  async listKeys(): Promise<ApiKeyWithDetails[]> {
    const results = await db
      .select({
        // API key fields
        id: apiKeys.id,
        keyHash: apiKeys.keyHash,
        keyPrefix: apiKeys.keyPrefix,
        keyType: apiKeys.keyType,
        organizationId: apiKeys.organizationId,
        projectId: apiKeys.projectId,
        scopes: apiKeys.scopes,
        createdAt: apiKeys.createdAt,
        expiresAt: apiKeys.expiresAt,
        lastUsedAt: apiKeys.lastUsedAt,
        isActive: apiKeys.isActive,
        name: apiKeys.name,
        createdBy: apiKeys.createdBy,
        // Organization fields
        orgId: organizations.id,
        orgSlug: organizations.slug,
        orgName: organizations.name,
        orgEmail: organizations.email,
        // Project fields
        projId: projects.id,
        projSlug: projects.slug,
        projName: projects.name,
      })
      .from(apiKeys)
      .leftJoin(organizations, eq(apiKeys.organizationId, organizations.id))
      .leftJoin(projects, eq(apiKeys.projectId, projects.id))
      .orderBy(desc(apiKeys.createdAt));

    // Transform flat results to nested structure
    return results.map((row) => ({
      id: row.id,
      keyHash: row.keyHash,
      keyPrefix: row.keyPrefix,
      keyType: row.keyType,
      organizationId: row.organizationId,
      projectId: row.projectId,
      scopes: row.scopes,
      createdAt: row.createdAt,
      expiresAt: row.expiresAt,
      lastUsedAt: row.lastUsedAt,
      isActive: row.isActive,
      name: row.name,
      createdBy: row.createdBy,
      organization:
        row.orgId && row.orgSlug && row.orgName && row.orgEmail
          ? {
              id: row.orgId,
              slug: row.orgSlug,
              name: row.orgName,
              email: row.orgEmail,
            }
          : null,
      project:
        row.projId && row.projSlug && row.projName
          ? {
              id: row.projId,
              slug: row.projSlug,
              name: row.projName,
            }
          : null,
    }));
  }

  /**
   * Check if any keys exist (for bootstrap)
   */
  async hasAnyKeys(): Promise<boolean> {
    const keys = await db.select({ id: apiKeys.id }).from(apiKeys).limit(1);

    return keys.length > 0;
  }

  /**
   * Get or create organization by slug
   * If organization doesn't exist, create it with provided name (or use slug as name)
   */
  async getOrCreateOrganization(slug: string, name?: string, email?: string): Promise<string> {
    // Try to find existing organization
    const [existing] = await db
      .select({ id: organizations.id })
      .from(organizations)
      .where(eq(organizations.slug, slug))
      .limit(1);

    if (existing) {
      return existing.id;
    }

    // Create new organization
    const [newOrg] = await db
      .insert(organizations)
      .values({
        slug,
        name: name || slug,
        email: email || `${slug}@placeholder.local`,
      })
      .returning({ id: organizations.id });

    console.log(`[${new Date().toISOString()}] Organization created: ${newOrg?.id} - ${slug}`);

    return newOrg!.id;
  }

  /**
   * Get or create project by slug within an organization
   * If project doesn't exist, create it with provided name (or use slug as name)
   */
  async getOrCreateProject(organizationId: string, slug: string, name?: string): Promise<string> {
    // Try to find existing project
    const [existing] = await db
      .select({ id: projects.id })
      .from(projects)
      .where(and(eq(projects.organizationId, organizationId), eq(projects.slug, slug)))
      .limit(1);

    if (existing) {
      return existing.id;
    }

    // Create new project
    const [newProject] = await db
      .insert(projects)
      .values({
        organizationId,
        slug,
        name: name || slug,
      })
      .returning({ id: projects.id });

    console.log(
      `[${new Date().toISOString()}] Project created: ${newProject?.id} - ${slug} (org: ${organizationId})`,
    );

    return newProject!.id;
  }
}
