import {
  Controller,
  Get,
  Post,
  Put,
  Delete,
  Param,
  Body,
  Query,
  UseInterceptors,
  Inject,
  NotFoundException,
  BadRequestException,
} from "@nestjs/common";
import {
  CacheInterceptor,
  CacheKey,
  CacheTTL,
  CACHE_MANAGER,
} from "@nestjs/cache-manager";
import * as cacheManager from "cache-manager";
import {
  ApiTags,
  ApiBearerAuth,
  ApiOperation,
  ApiResponse as SwaggerResponse,
} from "@nestjs/swagger";
import { Roles } from "../../common/decorators";
import { PrismaService } from "../../database/prisma.service";
import { PaginationDto } from "../../common/dto/pagination.dto";
import { AdminUsersQueryDto } from "./dto/admin-users-query.dto";
import { UpdateUserSubscriptionDto } from "./dto/update-user-subscription.dto";
import {
  ApiResponse,
  createSuccessResponse,
  createPaginatedResponse,
  PaginatedData,
} from "../../common/types/api-response.type";
import { plainToInstance } from "class-transformer";
import { UserResponseDto } from "../users/dto/user.dto";
import { UserRole } from "@prisma/client";
import { SubscriptionsService } from "../subscriptions/subscriptions.service";
import { PlanType } from "../subscriptions/dto/subscription.dto";

@ApiTags("Admin")
@ApiBearerAuth()
@Controller("admin")
@Roles("superadmin")
export class AdminController {
  constructor(
    private readonly prisma: PrismaService,
    @Inject(CACHE_MANAGER) private cacheManager: cacheManager.Cache,
    private readonly subscriptionsService: SubscriptionsService,
  ) {}

  // ================== Users Management ==================

  @Get("users")
  @ApiOperation({ summary: "Get all users (admin)" })
  @SwaggerResponse({ status: 200, type: [UserResponseDto] })
  async getAllUsers(
    @Query() query: AdminUsersQueryDto,
  ): Promise<ApiResponse<PaginatedData<UserResponseDto>>> {
    const { skip, take, orderBy, search, role, subscriptionStatus } = query;

    const where: any = {};
    if (search) {
      where.OR = [
        { email: { contains: search, mode: "insensitive" } },
        { firstName: { contains: search, mode: "insensitive" } },
        { lastName: { contains: search, mode: "insensitive" } },
      ];
    }
    if (role) {
      where.role = role;
    }
    if (subscriptionStatus) {
      where.subscriptionStatus = subscriptionStatus;
    }

    const [users, total] = await Promise.all([
      this.prisma.user.findMany({
        where,
        skip,
        take,
        orderBy,
      }),
      this.prisma.user.count({ where }),
    ]);

    const dtos = plainToInstance(
      UserResponseDto,
      users,
    ) as unknown as UserResponseDto[];

    return createPaginatedResponse(
      dtos,
      total,
      query.page || 1,
      query.limit || 10,
    );
  }

  @Get("users/:id")
  @ApiOperation({ summary: "Get user by ID" })
  @SwaggerResponse({ status: 200, type: UserResponseDto })
  async getUserById(
    @Param("id") id: string,
  ): Promise<ApiResponse<UserResponseDto>> {
    const user = await this.prisma.user.findUnique({
      where: { id },
      include: {
        usageLimit: true,
        analyses: {
          take: 5,
          orderBy: { createdAt: "desc" },
        },
      },
    });

    if (!user) {
      throw new NotFoundException("User not found");
    }

    return createSuccessResponse(plainToInstance(UserResponseDto, user));
  }

  @Put("users/:id/toggle-active")
  @ApiOperation({ summary: "Toggle user active status" })
  @SwaggerResponse({ status: 200, type: UserResponseDto })
  async toggleUserActive(
    @Param("id") id: string,
  ): Promise<ApiResponse<UserResponseDto>> {
    const user = await this.prisma.user.findUnique({ where: { id } });

    if (!user) {
      throw new NotFoundException("User not found");
    }

    const updated = await this.prisma.user.update({
      where: { id },
      data: { isActive: !user.isActive },
    });

    return createSuccessResponse(
      plainToInstance(UserResponseDto, updated),
      "common.SUCCESS_USER_STATUS_UPDATED",
    );
  }

  @Put("users/:id/role")
  @ApiOperation({ summary: "Update user role" })
  @SwaggerResponse({ status: 200, type: UserResponseDto })
  async updateUserRole(
    @Param("id") id: string,
    @Body() data: { role: UserRole },
  ): Promise<ApiResponse<UserResponseDto>> {
    const user = await this.prisma.user.update({
      where: { id },
      data: { role: data.role },
    });

    return createSuccessResponse(
      plainToInstance(UserResponseDto, user),
      "common.SUCCESS_USER_ROLE_UPDATED",
    );
  }

  @Delete("users/:id")
  @ApiOperation({ summary: "Soft delete a user" })
  @SwaggerResponse({ status: 200, description: "User deleted" })
  async deleteUser(@Param("id") id: string): Promise<ApiResponse<null>> {
    await this.prisma.user.update({
      where: { id },
      data: { deletedAt: new Date() },
    });
    return createSuccessResponse(null, "common.SUCCESS_USER_DELETED");
  }

  // ================== App Settings ==================

  @Get("settings")
  @UseInterceptors(CacheInterceptor)
  @CacheKey("app_settings")
  @CacheTTL(60 * 1000)
  @ApiOperation({ summary: "Get all app settings" })
  @SwaggerResponse({
    status: 200,
    schema: { type: "object", additionalProperties: { type: "string" } },
  })
  async getAllSettings(): Promise<ApiResponse<Record<string, string>>> {
    const settings = await this.prisma.appSetting.findMany();
    const settingsMap: Record<string, string> = {};
    for (const s of settings) {
      settingsMap[s.key] = s.value || "";
    }
    return createSuccessResponse(settingsMap);
  }

  @Put("settings/:key")
  @ApiOperation({ summary: "Update an app setting" })
  @SwaggerResponse({
    status: 200,
    schema: {
      type: "object",
      properties: { key: { type: "string" }, value: { type: "string" } },
    },
  })
  async updateSetting(
    @Param("key") key: string,
    @Body() data: { value: string },
  ): Promise<ApiResponse<{ key: string; value: string }>> {
    const setting = await this.prisma.appSetting.upsert({
      where: { key },
      update: { value: data.value },
      create: { key, value: data.value },
    });
    await this.cacheManager.del("app_settings");
    return createSuccessResponse(
      { key: setting.key, value: setting.value || "" },
      "common.SUCCESS_SETTING_UPDATED",
    );
  }

  // ================== Usage Limits ==================

  @Get("usage-limits")
  @ApiOperation({ summary: "Get all usage limits" })
  @SwaggerResponse({
    status: 200,
    schema: { type: "array", items: { type: "object" } },
  })
  async getAllUsageLimits(@Query() pagination: PaginationDto) {
    const { skip, take } = pagination;

    const [limits, total] = await Promise.all([
      this.prisma.usageLimit.findMany({
        skip,
        take,
        include: {
          user: {
            select: { id: true, email: true, firstName: true, lastName: true },
          },
        },
        orderBy: { lastResetDate: "desc" },
      }),
      this.prisma.usageLimit.count(),
    ]);

    return createPaginatedResponse(
      limits,
      total,
      pagination.page || 1,
      pagination.limit || 10,
    );
  }

  @Post("usage-limits/reset-all")
  @ApiOperation({ summary: "Reset all usage limits" })
  @SwaggerResponse({
    status: 200,
    schema: { type: "object", properties: { count: { type: "number" } } },
  })
  async resetAllUsageLimits(): Promise<ApiResponse<{ count: number }>> {
    const result = await this.prisma.usageLimit.updateMany({
      data: {
        analysisCount: 0,
        couponCount: 0,
        lastResetDate: new Date(),
      },
    });

    return createSuccessResponse(
      { count: result.count },
      "common.SUCCESS_ALL_LIMITS_RESET",
    );
  }

  @Post("usage-limits/reset/:userId")
  @ApiOperation({ summary: "Reset usage limits for a single user" })
  @SwaggerResponse({ status: 200 })
  async resetUserUsageLimits(
    @Param("userId") userId: string,
  ): Promise<ApiResponse<null>> {
    const user = await this.prisma.user.findUnique({ where: { id: userId } });
    if (!user) throw new NotFoundException("USER_NOT_FOUND");

    await this.prisma.usageLimit.update({
      where: { userId },
      data: {
        analysisCount: 0,
        couponCount: 0,
        lastResetDate: new Date(),
      },
    });

    return createSuccessResponse(null, "common.SUCCESS_USER_LIMITS_RESET");
  }

  @Put("users/:userId/subscription")
  @ApiOperation({ summary: "Update a user's subscription tier" })
  @SwaggerResponse({ status: 200 })
  async updateUserSubscription(
    @Param("userId") userId: string,
    @Body() data: UpdateUserSubscriptionDto,
  ): Promise<ApiResponse<null>> {
    const user = await this.prisma.user.findUnique({ where: { id: userId } });
    if (!user) throw new NotFoundException("USER_NOT_FOUND");

    const validPlans = [
      PlanType.FREE,
      PlanType.PLUS,
      PlanType.PREMIUM,
      "past_due",
      "cancelled",
    ];
    const newPlan = data.plan as PlanType;
    if (!validPlans.includes(newPlan)) {
      throw new BadRequestException("INVALID_PLAN_TYPE");
    }

    const updateData: any = { subscriptionStatus: newPlan };

    if (data.expiresAt) {
      const parsedDate = new Date(data.expiresAt);

      // Business Logic: If upgrading to Premium/Plus, the expiry date cannot be in the past
      const today = new Date();
      today.setHours(0, 0, 0, 0); // Strip time

      const expiry = new Date(parsedDate);
      expiry.setHours(0, 0, 0, 0);

      if (
        (newPlan === PlanType.PREMIUM || newPlan === PlanType.PLUS) &&
        expiry < today
      ) {
        throw new BadRequestException("EXPIRES_AT_CANNOT_BE_IN_PAST");
      }

      updateData.subscriptionExpiresAt = parsedDate;
    } else if (data.expiresAt === null) {
      updateData.subscriptionExpiresAt = null;
    }

    await this.prisma.user.update({
      where: { id: userId },
      data: updateData,
    });

    await this.subscriptionsService.syncLimitsWithPlan(userId, newPlan);

    return createSuccessResponse(
      null,
      "common.SUCCESS_USER_SUBSCRIPTION_UPDATED",
    );
  }

  // ================== Analytics ==================

  @Get("analytics/overview")
  @ApiOperation({ summary: "Get system analytics overview" })
  @SwaggerResponse({ status: 200, schema: { type: "object" } })
  async getAnalyticsOverview() {
    const [
      totalUsers,
      activeUsers,
      premiumUsers,
      totalMatches,
      totalPredictions,
      totalCoupons,
    ] = await Promise.all([
      this.prisma.user.count(),
      this.prisma.user.count({ where: { isActive: true } }),
      this.prisma.user.count({
        where: { subscriptionStatus: { in: ["plus", "premium"] } },
      }),
      this.prisma.match.count(),
      this.prisma.prediction.count(),
      this.prisma.userCoupon.count(),
    ]);

    return createSuccessResponse({
      totalUsers,
      activeUsers,
      totalPredictions,
      totalCoupons,
      users: {
        total: totalUsers,
        active: activeUsers,
        premium: premiumUsers,
      },
      matches: totalMatches,
      predictions: totalPredictions,
    });
  }

  // ================== Model Performance (Forward-Test) ==================

  @Get("model-performance")
  @ApiOperation({
    summary:
      "Per-market calibration (model% vs actual%), ROI and decision rationale " +
      "from settled prediction_runs. Powers the admin Model Performance page.",
  })
  @SwaggerResponse({ status: 200, schema: { type: "object" } })
  async getModelPerformance(
    @Query("days") daysRaw?: string,
  ): Promise<ApiResponse<ModelPerformanceResult>> {
    const days = Math.min(Math.max(Number(daysRaw) || 90, 1), 1000);
    const sinceMs = Date.now() - days * 24 * 60 * 60 * 1000;

    // Pull settled rows in window. markets_settled holds one entry per market.
    const rows = await this.prisma.$queryRawUnsafe<
      Array<{ payload_summary: unknown; generated_at: Date }>
    >(
      `
        SELECT pr.payload_summary, pr.generated_at
        FROM prediction_runs pr
        WHERE pr.eventual_outcome IS NOT NULL
          AND pr.generated_at >= $1
          AND pr.payload_summary -> 'settlement' -> 'markets_settled' IS NOT NULL
        ORDER BY pr.generated_at DESC
        LIMIT 50000
      `,
      new Date(sinceMs),
    );

    // ── Aggregate per market ──────────────────────────────────────────
    type Acc = {
      market: string;
      n: number;
      wins: number;
      sumShown: number; // Σ shown_confidence (0–100)
      shownCount: number;
      // 10-bin reliability for ECE
      bins: Array<{ sumP: number; sumY: number; n: number }>;
      // betting (only playable BET rows with odds)
      betN: number;
      betWins: number;
      betProfit: number;
      betStake: number;
      // rationale tally
      actions: Record<string, number>;
      tiers: Record<string, number>;
    };
    const acc = new Map<string, Acc>();
    const ensure = (mk: string): Acc => {
      let a = acc.get(mk);
      if (!a) {
        a = {
          market: mk,
          n: 0,
          wins: 0,
          sumShown: 0,
          shownCount: 0,
          bins: Array.from({ length: 10 }, () => ({ sumP: 0, sumY: 0, n: 0 })),
          betN: 0,
          betWins: 0,
          betProfit: 0,
          betStake: 0,
          actions: {},
          tiers: {},
        };
        acc.set(mk, a);
      }
      return a;
    };

    let totalSettledMarkets = 0;
    for (const row of rows) {
      const summary =
        row.payload_summary && typeof row.payload_summary === "object"
          ? (row.payload_summary as Record<string, unknown>)
          : {};
      const settlement =
        summary.settlement && typeof summary.settlement === "object"
          ? (summary.settlement as Record<string, unknown>)
          : {};
      const markets = Array.isArray(settlement.markets_settled)
        ? (settlement.markets_settled as Array<Record<string, unknown>>)
        : [];

      for (const m of markets) {
        const market = typeof m.market === "string" ? m.market : "";
        if (!market) continue;
        const won = m.won === true;
        const shown =
          m.shown_confidence != null ? Number(m.shown_confidence) : null;
        const a = ensure(market);
        a.n += 1;
        if (won) a.wins += 1;
        totalSettledMarkets += 1;

        if (shown != null && Number.isFinite(shown)) {
          a.sumShown += shown;
          a.shownCount += 1;
          const p = Math.min(Math.max(shown / 100, 0), 0.999999);
          const bi = Math.min(9, Math.floor(p * 10));
          a.bins[bi].sumP += p;
          a.bins[bi].sumY += won ? 1 : 0;
          a.bins[bi].n += 1;
        }

        const odds = m.odds != null ? Number(m.odds) : null;
        const isBet = m.playable === true && m.action === "BET";
        if (isBet && odds != null && Number.isFinite(odds) && odds > 1.01) {
          a.betN += 1;
          if (won) {
            a.betWins += 1;
            a.betProfit += odds - 1;
          } else {
            a.betProfit -= 1;
          }
          a.betStake += 1;
        }

        const action = typeof m.action === "string" ? m.action : "—";
        a.actions[action] = (a.actions[action] ?? 0) + 1;
        const tier = typeof m.value_tier === "string" ? m.value_tier : "—";
        a.tiers[tier] = (a.tiers[tier] ?? 0) + 1;
      }
    }

    const markets = Array.from(acc.values())
      .map((a) => {
        const actualPct = a.n > 0 ? (a.wins / a.n) * 100 : 0;
        const shownPct = a.shownCount > 0 ? a.sumShown / a.shownCount : 0;
        // ECE: Σ |acc_bin - conf_bin| * (n_bin / N)
        let ece = 0;
        for (const b of a.bins) {
          if (b.n === 0) continue;
          const conf = b.sumP / b.n;
          const acc2 = b.sumY / b.n;
          ece += Math.abs(acc2 - conf) * (b.n / a.shownCount || 0);
        }
        return {
          market: a.market,
          samples: a.n,
          shown_pct: Number(shownPct.toFixed(1)),
          actual_pct: Number(actualPct.toFixed(1)),
          gap: Number((shownPct - actualPct).toFixed(1)),
          ece: Number((ece * 100).toFixed(1)),
          calibration: (Math.abs(shownPct - actualPct) <= 4
            ? "good"
            : shownPct > actualPct
              ? "overconfident"
              : "underconfident") as
            | "good"
            | "overconfident"
            | "underconfident",
          bet_count: a.betN,
          bet_hit_pct:
            a.betN > 0 ? Number(((a.betWins / a.betN) * 100).toFixed(1)) : 0,
          bet_roi_pct:
            a.betStake > 0
              ? Number(((a.betProfit / a.betStake) * 100).toFixed(1))
              : 0,
          actions: a.actions,
          tiers: a.tiers,
        };
      })
      .sort((x, y) => y.samples - x.samples);

    const result: ModelPerformanceResult = {
      window_days: days,
      settled_runs: Number(rows.length),
      settled_markets: totalSettledMarkets,
      generated_at: new Date().toISOString(),
      markets,
    };
    return createSuccessResponse(result);
  }
}

interface ModelPerformanceResult {
  window_days: number;
  settled_runs: number;
  settled_markets: number;
  generated_at: string;
  markets: Array<{
    market: string;
    samples: number;
    shown_pct: number;
    actual_pct: number;
    gap: number;
    ece: number;
    calibration: "good" | "overconfident" | "underconfident";
    bet_count: number;
    bet_hit_pct: number;
    bet_roi_pct: number;
    actions: Record<string, number>;
    tiers: Record<string, number>;
  }>;
}
