import { Repository, IsNull } from 'typeorm';
import { randomBytes } from 'crypto';
import { OAuthProvider, OAuthState, OAuthUserLink, User } from '../entities';

export interface ServiceContext {
  tenantId: string;
  userId?: string;
}

export interface CreateOAuthProviderDto {
  name: string;
  provider: string;
  clientId: string;
  clientSecret: string;
  authorizationUrl?: string;
  tokenUrl?: string;
  userInfoUrl?: string;
  scopes?: string[];
  isActive?: boolean;
}

export interface UpdateOAuthProviderDto {
  name?: string;
  clientId?: string;
  clientSecret?: string;
  authorizationUrl?: string;
  tokenUrl?: string;
  userInfoUrl?: string;
  scopes?: string[];
  isActive?: boolean;
}

export interface OAuthProviderFilters {
  provider?: string;
  isActive?: boolean;
  search?: string;
}

export interface PaginatedResult<T> {
  data: T[];
  total: number;
  page: number;
  limit: number;
  totalPages: number;
}

export interface OAuthStateData {
  redirectUri?: string;
  returnUrl?: string;
  metadata?: Record<string, unknown>;
}

export class OAuthService {
  constructor(
    private readonly providerRepository: Repository<OAuthProvider>,
    private readonly stateRepository: Repository<OAuthState>,
    private readonly userLinkRepository: Repository<OAuthUserLink>,
    private readonly userRepository: Repository<User>,
  ) {}

  // Provider Management
  async createProvider(ctx: ServiceContext, data: CreateOAuthProviderDto): Promise<OAuthProvider> {
    const existing = await this.providerRepository.findOne({
      where: { tenantId: ctx.tenantId, provider: data.provider, deletedAt: IsNull() },
    });

    if (existing) {
      throw new Error(`OAuth provider '${data.provider}' already configured`);
    }

    const provider = this.providerRepository.create({
      tenantId: ctx.tenantId,
      name: data.name,
      provider: data.provider,
      clientId: data.clientId,
      clientSecret: data.clientSecret,
      authorizationUrl: data.authorizationUrl,
      tokenUrl: data.tokenUrl,
      userInfoUrl: data.userInfoUrl,
      scopes: data.scopes || [],
      isActive: data.isActive ?? true,
      createdBy: ctx.userId ?? null,
    });

    return this.providerRepository.save(provider);
  }

  async findProviderById(ctx: ServiceContext, id: string): Promise<OAuthProvider | null> {
    return this.providerRepository.findOne({
      where: { id, tenantId: ctx.tenantId, deletedAt: IsNull() },
    });
  }

  async findProviderByName(ctx: ServiceContext, provider: string): Promise<OAuthProvider | null> {
    return this.providerRepository.findOne({
      where: { tenantId: ctx.tenantId, provider, deletedAt: IsNull() },
    });
  }

  async findAllProviders(ctx: ServiceContext): Promise<OAuthProvider[]> {
    return this.providerRepository.find({
      where: { tenantId: ctx.tenantId, deletedAt: IsNull() },
      order: { name: 'ASC' },
    });
  }

  async findActiveProviders(ctx: ServiceContext): Promise<OAuthProvider[]> {
    return this.providerRepository.find({
      where: { tenantId: ctx.tenantId, isActive: true, deletedAt: IsNull() },
      order: { name: 'ASC' },
    });
  }

  async findProvidersWithFilters(
    ctx: ServiceContext,
    filters: OAuthProviderFilters,
    page = 1,
    limit = 50,
  ): Promise<PaginatedResult<OAuthProvider>> {
    const qb = this.providerRepository
      .createQueryBuilder('provider')
      .where('provider.tenant_id = :tenantId', { tenantId: ctx.tenantId })
      .andWhere('provider.deleted_at IS NULL');

    if (filters.provider) {
      qb.andWhere('provider.provider = :provider', { provider: filters.provider });
    }

    if (filters.isActive !== undefined) {
      qb.andWhere('provider.is_active = :isActive', { isActive: filters.isActive });
    }

    if (filters.search) {
      qb.andWhere('(provider.name ILIKE :search OR provider.provider ILIKE :search)', {
        search: `%${filters.search}%`,
      });
    }

    const [data, total] = await qb
      .orderBy('provider.name', 'ASC')
      .skip((page - 1) * limit)
      .take(limit)
      .getManyAndCount();

    return {
      data,
      total,
      page,
      limit,
      totalPages: Math.ceil(total / limit),
    };
  }

  async updateProvider(ctx: ServiceContext, id: string, data: UpdateOAuthProviderDto): Promise<OAuthProvider | null> {
    const provider = await this.findProviderById(ctx, id);
    if (!provider) return null;

    Object.assign(provider, {
      ...data,
      updatedBy: ctx.userId ?? null,
    });

    return this.providerRepository.save(provider);
  }

  async deleteProvider(ctx: ServiceContext, id: string): Promise<boolean> {
    const provider = await this.findProviderById(ctx, id);
    if (!provider) return false;

    provider.deletedAt = new Date();
    provider.deletedBy = ctx.userId ?? null;
    await this.providerRepository.save(provider);

    return true;
  }

  // OAuth State Management
  async createState(ctx: ServiceContext, providerId: string, data?: OAuthStateData): Promise<OAuthState> {
    const state = randomBytes(32).toString('hex');
    const expiresAt = new Date(Date.now() + 10 * 60 * 1000); // 10 minutes

    const oauthState = this.stateRepository.create({
      tenantId: ctx.tenantId,
      providerId,
      state,
      redirectUri: data?.redirectUri,
      returnUrl: data?.returnUrl,
      metadata: data?.metadata,
      expiresAt,
    });

    return this.stateRepository.save(oauthState);
  }

  async validateState(ctx: ServiceContext, state: string): Promise<OAuthState | null> {
    const oauthState = await this.stateRepository.findOne({
      where: { tenantId: ctx.tenantId, state, isUsed: false },
      relations: ['provider'],
    });

    if (!oauthState) return null;
    if (oauthState.expiresAt < new Date()) return null;

    return oauthState;
  }

  async consumeState(ctx: ServiceContext, state: string): Promise<OAuthState | null> {
    const oauthState = await this.validateState(ctx, state);
    if (!oauthState) return null;

    oauthState.isUsed = true;
    oauthState.usedAt = new Date();
    await this.stateRepository.save(oauthState);

    return oauthState;
  }

  // User Link Management
  async linkUser(
    ctx: ServiceContext,
    userId: string,
    providerId: string,
    providerUserId: string,
    profile?: Record<string, unknown>,
  ): Promise<OAuthUserLink> {
    const existing = await this.userLinkRepository.findOne({
      where: {
        tenantId: ctx.tenantId,
        providerId,
        providerUserId,
      },
    });

    if (existing) {
      existing.profile = profile || existing.profile;
      existing.updatedBy = ctx.userId ?? null;
      return this.userLinkRepository.save(existing);
    }

    const link = this.userLinkRepository.create({
      tenantId: ctx.tenantId,
      userId,
      providerId,
      providerUserId,
      profile,
      createdBy: ctx.userId ?? null,
    });

    return this.userLinkRepository.save(link);
  }

  async findUserByProviderLink(
    ctx: ServiceContext,
    providerId: string,
    providerUserId: string,
  ): Promise<User | null> {
    const link = await this.userLinkRepository.findOne({
      where: {
        tenantId: ctx.tenantId,
        providerId,
        providerUserId,
      },
      relations: ['user'],
    });

    return link?.user || null;
  }

  async findUserLinks(ctx: ServiceContext, userId: string): Promise<OAuthUserLink[]> {
    return this.userLinkRepository.find({
      where: { tenantId: ctx.tenantId, userId },
      relations: ['provider'],
    });
  }

  async unlinkUser(ctx: ServiceContext, userId: string, providerId: string): Promise<boolean> {
    const result = await this.userLinkRepository.delete({
      tenantId: ctx.tenantId,
      userId,
      providerId,
    });

    return (result.affected ?? 0) > 0;
  }

  async cleanupExpiredStates(ctx: ServiceContext): Promise<number> {
    const result = await this.stateRepository.delete({
      tenantId: ctx.tenantId,
      expiresAt: IsNull(),
    });

    // Also clean old used states
    const cleanupDate = new Date(Date.now() - 24 * 60 * 60 * 1000);
    const result2 = await this.stateRepository
      .createQueryBuilder()
      .delete()
      .where('tenant_id = :tenantId', { tenantId: ctx.tenantId })
      .andWhere('is_used = true')
      .andWhere('used_at < :cleanupDate', { cleanupDate })
      .execute();

    return (result.affected ?? 0) + (result2.affected ?? 0);
  }
}
