import {
  Body,
  Controller,
  Delete,
  ForbiddenException,
  Get,
  HttpCode,
  HttpStatus,
  Inject,
  NotFoundException,
  Param,
  Patch,
  Post,
  UseGuards,
} from '@nestjs/common';
import { routingRules, type Db, and, asc, eq, or, inArray } from '@mosaicstack/db';
import { DB } from '../../database/database.module.js';
import { AuthGuard } from '../../auth/auth.guard.js';
import { CurrentUser } from '../../auth/current-user.decorator.js';
import {
  CreateRoutingRuleDto,
  UpdateRoutingRuleDto,
  ReorderRoutingRulesDto,
} from './routing.dto.js';

@Controller('api/routing/rules')
@UseGuards(AuthGuard)
export class RoutingController {
  constructor(@Inject(DB) private readonly db: Db) {}

  /**
   * GET /api/routing/rules
   * List all rules visible to the authenticated user:
   * - All system rules
   * - User's own rules
   * Ordered by priority ascending (lower number = higher priority).
   */
  @Get()
  async list(@CurrentUser() user: { id: string }) {
    const rows = await this.db
      .select()
      .from(routingRules)
      .where(
        or(
          eq(routingRules.scope, 'system'),
          and(eq(routingRules.scope, 'user'), eq(routingRules.userId, user.id)),
        ),
      )
      .orderBy(asc(routingRules.priority));

    return rows;
  }

  /**
   * GET /api/routing/rules/effective
   * Return the merged rule set in priority order.
   * User-scoped rules are checked before system rules at the same priority
   * (achieved by ordering: priority ASC, then scope='user' first).
   */
  @Get('effective')
  async effective(@CurrentUser() user: { id: string }) {
    const rows = await this.db
      .select()
      .from(routingRules)
      .where(
        and(
          eq(routingRules.enabled, true),
          or(
            eq(routingRules.scope, 'system'),
            and(eq(routingRules.scope, 'user'), eq(routingRules.userId, user.id)),
          ),
        ),
      )
      .orderBy(asc(routingRules.priority));

    // For rules with the same priority: user rules beat system rules.
    // Group by priority then stable-sort each group: user before system.
    const grouped = new Map<number, typeof rows>();
    for (const row of rows) {
      const bucket = grouped.get(row.priority) ?? [];
      bucket.push(row);
      grouped.set(row.priority, bucket);
    }

    const effective: typeof rows = [];
    for (const [, bucket] of [...grouped.entries()].sort(([a], [b]) => a - b)) {
      // user-scoped rules first within the same priority bucket
      const userRules = bucket.filter((r) => r.scope === 'user');
      const systemRules = bucket.filter((r) => r.scope === 'system');
      effective.push(...userRules, ...systemRules);
    }

    return effective;
  }

  /**
   * POST /api/routing/rules
   * Create a new routing rule. Scope is forced to 'user' (users cannot create
   * system rules). The authenticated user's ID is attached automatically.
   */
  @Post()
  async create(@Body() dto: CreateRoutingRuleDto, @CurrentUser() user: { id: string }) {
    const [created] = await this.db
      .insert(routingRules)
      .values({
        name: dto.name,
        priority: dto.priority,
        scope: 'user',
        userId: user.id,
        conditions: dto.conditions as unknown as Record<string, unknown>[],
        action: dto.action as unknown as Record<string, unknown>,
        enabled: dto.enabled ?? true,
      })
      .returning();

    return created;
  }

  /**
   * PATCH /api/routing/rules/reorder
   * Reassign priorities so that the order of `ruleIds` reflects ascending
   * priority (index 0 = priority 0, index 1 = priority 1, …).
   * Only the authenticated user's own rules can be reordered.
   */
  @Patch('reorder')
  async reorder(@Body() dto: ReorderRoutingRulesDto, @CurrentUser() user: { id: string }) {
    // Verify all supplied IDs belong to this user
    const owned = await this.db
      .select({ id: routingRules.id })
      .from(routingRules)
      .where(
        and(
          inArray(routingRules.id, dto.ruleIds),
          eq(routingRules.scope, 'user'),
          eq(routingRules.userId, user.id),
        ),
      );

    const ownedIds = new Set(owned.map((r) => r.id));
    const unowned = dto.ruleIds.filter((id) => !ownedIds.has(id));
    if (unowned.length > 0) {
      throw new ForbiddenException(
        `Cannot reorder rules that do not belong to you: ${unowned.join(', ')}`,
      );
    }

    // Apply new priorities in transaction
    const updates = await this.db.transaction(async (tx) => {
      const results = [];
      for (let i = 0; i < dto.ruleIds.length; i++) {
        const [updated] = await tx
          .update(routingRules)
          .set({ priority: i, updatedAt: new Date() })
          .where(and(eq(routingRules.id, dto.ruleIds[i]!), eq(routingRules.userId, user.id)))
          .returning();
        if (updated) results.push(updated);
      }
      return results;
    });

    return updates;
  }

  /**
   * PATCH /api/routing/rules/:id
   * Update a user-owned rule. System rules cannot be modified by regular users.
   */
  @Patch(':id')
  async update(
    @Param('id') id: string,
    @Body() dto: UpdateRoutingRuleDto,
    @CurrentUser() user: { id: string },
  ) {
    const [existing] = await this.db.select().from(routingRules).where(eq(routingRules.id, id));

    if (!existing) throw new NotFoundException('Routing rule not found');

    if (existing.scope === 'system') {
      throw new ForbiddenException('System routing rules cannot be modified');
    }

    if (existing.userId !== user.id) {
      throw new ForbiddenException('Routing rule does not belong to the current user');
    }

    const updatePayload: Partial<typeof routingRules.$inferInsert> = {
      updatedAt: new Date(),
    };

    if (dto.name !== undefined) updatePayload.name = dto.name;
    if (dto.priority !== undefined) updatePayload.priority = dto.priority;
    if (dto.conditions !== undefined)
      updatePayload.conditions = dto.conditions as unknown as Record<string, unknown>[];
    if (dto.action !== undefined)
      updatePayload.action = dto.action as unknown as Record<string, unknown>;
    if (dto.enabled !== undefined) updatePayload.enabled = dto.enabled;

    const [updated] = await this.db
      .update(routingRules)
      .set(updatePayload)
      .where(and(eq(routingRules.id, id), eq(routingRules.userId, user.id)))
      .returning();

    if (!updated) throw new NotFoundException('Routing rule not found');
    return updated;
  }

  /**
   * DELETE /api/routing/rules/:id
   * Delete a user-owned routing rule. System rules cannot be deleted.
   */
  @Delete(':id')
  @HttpCode(HttpStatus.NO_CONTENT)
  async remove(@Param('id') id: string, @CurrentUser() user: { id: string }) {
    const [existing] = await this.db.select().from(routingRules).where(eq(routingRules.id, id));

    if (!existing) throw new NotFoundException('Routing rule not found');

    if (existing.scope === 'system') {
      throw new ForbiddenException('System routing rules cannot be deleted');
    }

    if (existing.userId !== user.id) {
      throw new ForbiddenException('Routing rule does not belong to the current user');
    }

    const [deleted] = await this.db
      .delete(routingRules)
      .where(and(eq(routingRules.id, id), eq(routingRules.userId, user.id)))
      .returning();

    if (!deleted) throw new NotFoundException('Routing rule not found');
  }
}
