import {
  Controller,
  Get,
  Post,
  Patch,
  Delete,
  Body,
  Param,
  Query,
  Headers,
  UseGuards,
  UseInterceptors,
  UploadedFile,
} from '@nestjs/common';
import { FileInterceptor } from '@nestjs/platform-express';
import { ApiTags, ApiBearerAuth } from '@nestjs/swagger';
import { randomUUID } from 'crypto';
import { mkdir, writeFile } from 'fs/promises';
import { extname, join } from 'path';
import { JwtAuthGuard, PlayerGuard } from '../../domains/identity/guards';
import { CurrentUser, Public } from '../../shared/common/decorators';
import { jsonResponse } from '../../shared/common/filters';
import { appBadRequest } from '../../shared/common/app-error';
import { getUploadRoot } from '../../shared/uploads/upload-paths';
import { UsersService } from '../../domains/identity/users.service';
import { SystemConfigService } from '../../shared/config/system-config.service';
import { WalletService } from '../../domains/ledger/wallet.service';
import { MatchesService } from '../../domains/catalog/matches.service';
import { OutrightService } from '../../domains/catalog/outright.service';
import { BetsService } from '../../domains/betting/bets.service';
import { ContentService } from '../../domains/operations/content/content.service';
import { CashbackService } from '../../domains/operations/cashback/cashback.service';
import { DepositService } from '../../domains/deposit/deposit.service';
import { PlayerMessagesService } from '../../domains/player-messages/player-messages.service';
import { PresenceService } from '../../domains/presence/presence.service';
import { isInLocalTodayMatchWindow } from '@thebet365/shared';
import { IsString, IsNumber, IsArray, ValidateNested, Min, IsOptional } from 'class-validator';
import { Type } from 'class-transformer';

class SingleBetDto {
  @IsString()
  selectionId!: string;

  @IsString()
  oddsVersion!: string;

  @IsNumber()
  @Min(0.01)
  stake!: number;

  @IsString()
  requestId!: string;
}

class ParlayLegDto {
  @IsString()
  selectionId!: string;

  @IsString()
  oddsVersion!: string;
}

class ParlayBetDto {
  @IsArray()
  @ValidateNested({ each: true })
  @Type(() => ParlayLegDto)
  legs!: ParlayLegDto[];

  @IsNumber()
  @Min(0.01)
  stake!: number;

  @IsString()
  requestId!: string;
}

class LocaleDto {
  @IsString()
  locale!: string;
}

class UpdateProfileDto {
  @IsOptional()
  @IsString()
  phone?: string;

  @IsOptional()
  @IsString()
  email?: string;

  @IsOptional()
  @IsString()
  avatarKey?: string;

  @IsOptional()
  @IsString()
  username?: string;
}

function safeTimeZone(input?: string): string {
  const value = input?.trim();
  if (!value) return 'UTC';
  try {
    new Intl.DateTimeFormat('en-US', { timeZone: value }).format(new Date());
    return value;
  } catch {
    return 'UTC';
  }
}

@ApiTags('Player')
@Controller('player')
@UseGuards(JwtAuthGuard, PlayerGuard)
@ApiBearerAuth()
export class PlayerController {
  constructor(
    private users: UsersService,
    private wallet: WalletService,
    private matches: MatchesService,
    private outright: OutrightService,
    private bets: BetsService,
    private content: ContentService,
    private cashback: CashbackService,
    private systemConfig: SystemConfigService,
    private deposit: DepositService,
    private playerMessages: PlayerMessagesService,
    private presence: PresenceService,
  ) {}

  @Post('presence/ping')
  async presencePing(@CurrentUser('id') userId: bigint) {
    await this.presence.touch(userId);
    return jsonResponse({ ok: true });
  }

  private async formatPlayerProfile(user: NonNullable<Awaited<ReturnType<UsersService['findById']>>>) {
    const accountSettings = await this.systemConfig.getPlayerAccountSettings();
    const prefs = user.preferences;
    const viewablePassword = prefs?.managedPassword ?? null;
    const safePrefs = prefs
      ? (({
          managedPassword: _m,
          allowPasswordChange: _a,
          allowUsernameChange: _b,
          ...rest
        }) => rest)(prefs)
      : {};
    return {
      ...user,
      id: user.id.toString(),
      parentId: user.parentId?.toString() ?? null,
      preferences: {
        ...safePrefs,
        viewablePassword,
        allowPasswordChange: accountSettings.allowPasswordChange,
        allowUsernameChange: accountSettings.allowUsernameChange,
      },
    };
  }

  @Get('profile')
  async profile(@CurrentUser('id') userId: bigint) {
    const user = await this.users.findById(userId);
    if (!user) return jsonResponse(null);
    return jsonResponse(await this.formatPlayerProfile(user));
  }

  @Post('language')
  async setLanguage(@CurrentUser('id') userId: bigint, @Body() dto: LocaleDto) {
    const result = await this.users.updateLocale(userId, dto.locale);
    return jsonResponse(result);
  }

  @Patch('profile')
  async updateProfile(@CurrentUser('id') userId: bigint, @Body() dto: UpdateProfileDto) {
    const user = await this.users.updateProfile(userId, dto);
    if (!user) return jsonResponse(null);
    return jsonResponse(await this.formatPlayerProfile(user));
  }

  @Public()
  @Get('home')
  async home(
    @CurrentUser('locale') userLocale: string | undefined,
    @Headers('x-locale') headerLocale?: string,
    @Headers('x-time-zone') headerTimeZone?: string,
  ) {
    const locale = userLocale || headerLocale || 'zh-CN';
    const [banners, announcements, allMatches, upcomingMatches, inboxEnabled] = await Promise.all([
      this.content.listActive('BANNER', locale),
      this.content.listActiveAnnouncements(locale),
      this.matches.listPublished(locale, undefined, { includeMarkets: false }),
      this.matches.listUpcomingPublished(locale),
      this.systemConfig.getInboxFeatureEnabled(),
    ]);
    const timeZone = safeTimeZone(headerTimeZone);
    const now = new Date();
    const hotMatches = (allMatches as Array<{ isHot?: boolean; status?: string }>).filter(
      (m) => m.isHot && m.status !== 'SETTLED',
    );
    const todayMatches = (allMatches as Array<{ startTime: string }>).filter((m) => {
      const kickoff = new Date(m.startTime);
      return !Number.isNaN(kickoff.getTime()) && isInLocalTodayMatchWindow(kickoff, now, timeZone);
    });
    return jsonResponse({
      banners,
      announcements,
      /** @deprecated 使用 announcements */
      ticker: announcements,
      /** @deprecated 使用 announcements */
      notices: announcements,
      hotMatches,
      todayMatches,
      upcomingMatches,
      inboxEnabled,
    });
  }

  @Public()
  @Get('matches')
  async listMatches(
    @CurrentUser('locale') userLocale: string | undefined,
    @Headers('x-locale') headerLocale: string | undefined,
    @Query('leagueId') leagueId?: string,
    @Query('scope') scope?: string,
  ) {
    const locale = userLocale || headerLocale || 'zh-CN';
    const lid = leagueId ? BigInt(leagueId) : undefined;
    if (scope === 'parlay') {
      const items = await this.matches.listPublished(locale, lid, {
        includeMarkets: true,
        parlayMarketsOnly: true,
      });
      return jsonResponse(items);
    }
    const items = await this.matches.listPublished(locale, lid, { includeMarkets: false });
    return jsonResponse(items);
  }

  @Public()
  @Get('outrights')
  async listOutrights(
    @CurrentUser('locale') userLocale: string | undefined,
    @Headers('x-locale') headerLocale: string | undefined,
  ) {
    const locale = userLocale || headerLocale || 'zh-CN';
    const items = await this.outright.listForPlayer(locale);
    return jsonResponse(items);
  }

  @Public()
  @Get('matches/:id')
  async matchDetail(
    @Param('id') id: string,
    @CurrentUser('locale') userLocale: string | undefined,
    @Headers('x-locale') headerLocale: string | undefined,
  ) {
    const locale = userLocale || headerLocale || 'zh-CN';
    const match = await this.matches.getMatchDetail(BigInt(id), locale);
    return jsonResponse(match);
  }

  @Public()
  @Get('selections/odds')
  async selectionOdds(@Query('ids') ids?: string) {
    if (!ids?.trim()) throw appBadRequest('SELECTION_NOT_FOUND');
    const parsed = ids
      .split(',')
      .map((s) => s.trim())
      .filter(Boolean)
      .map((s) => BigInt(s));
    if (!parsed.length || parsed.length > 20) throw appBadRequest('PARLAY_LEG_COUNT_INVALID');
    const items = await this.matches.getSelectionsOdds(parsed);
    return jsonResponse({ items });
  }

  @Post('bets/single')
  async singleBet(@CurrentUser('id') userId: bigint, @CurrentUser('parentId') parentId: bigint, @Body() dto: SingleBetDto) {
    const bet = await this.bets.placeSingleBet(
      userId,
      parentId,
      BigInt(dto.selectionId),
      BigInt(dto.oddsVersion),
      dto.stake,
      dto.requestId,
    );
    return jsonResponse(bet);
  }

  @Post('bets/parlay')
  async parlayBet(@CurrentUser('id') userId: bigint, @CurrentUser('parentId') parentId: bigint, @Body() dto: ParlayBetDto) {
    const bet = await this.bets.placeParlayBet(
      userId,
      parentId,
      dto.legs.map((l) => ({ selectionId: BigInt(l.selectionId), oddsVersion: BigInt(l.oddsVersion) })),
      dto.stake,
      dto.requestId,
    );
    return jsonResponse(bet);
  }

  @Get('bets')
  async myBets(
    @CurrentUser('id') userId: bigint,
    @CurrentUser('locale') locale: string,
    @Query('status') status?: string,
    @Query('page') page?: string,
    @Query('matchId') matchId?: string,
  ) {
    const result = await this.bets.getUserBets(
      userId,
      status,
      page ? parseInt(page, 10) : 1,
      20,
      matchId ? BigInt(matchId) : undefined,
    );
    const items = await this.matches.enrichBetsForHistory(result.items, locale);
    return jsonResponse({ ...result, items });
  }

  @Get('bets/stats')
  async betStats(@CurrentUser('id') userId: bigint) {
    const stats = await this.bets.getUserBetStats(userId);
    return jsonResponse(stats);
  }

  @Get('bets/:betNo')
  async betDetail(
    @CurrentUser('id') userId: bigint,
    @CurrentUser('locale') locale: string,
    @Param('betNo') betNo: string,
  ) {
    const bet = await this.bets.getBetByNo(betNo, userId);
    if (!bet) return jsonResponse(null);
    const [enriched] = await this.matches.enrichBetsForHistory([bet], locale);
    return jsonResponse(enriched);
  }

  @Get('wallet/transactions/stats')
  async transactionStats(@CurrentUser('id') userId: bigint) {
    const stats = await this.wallet.getTransactionStats(userId);
    return jsonResponse(stats);
  }

  @Get('wallet/transactions')
  async transactions(
    @CurrentUser('id') userId: bigint,
    @Query('page') page?: string,
    @Query('type') type?: string,
  ) {
    const result = await this.wallet.getTransactions(userId, page ? parseInt(page) : 1, 20, type);
    return jsonResponse(result);
  }

  @Get('wallet/transactions/:transactionId')
  async transactionDetail(
    @CurrentUser('id') userId: bigint,
    @Param('transactionId') transactionId: string,
  ) {
    const detail = await this.wallet.getTransactionDetail(userId, transactionId);
    return jsonResponse(detail);
  }

  @Get('cashbacks')
  async cashbacks(@CurrentUser('id') userId: bigint) {
    const items = await this.cashback.getUserCashbacks(userId);
    return jsonResponse(items);
  }

  // ============ Deposit / Recharge ============

  @Get('payment-methods')
  async paymentMethods(
    @Query('methodType') methodType?: string,
    @CurrentUser('locale') userLocale?: string,
    @Headers('x-locale') headerLocale?: string,
  ) {
    const locale = userLocale || headerLocale || 'zh-CN';
    const items = await this.deposit.listPlayerPaymentMethods(methodType || undefined, locale);
    return jsonResponse(items);
  }

  @Post('deposit-orders')
  @UseInterceptors(FileInterceptor('screenshot', { limits: { fileSize: 5 * 1024 * 1024 } }))
  async createDepositOrder(
    @CurrentUser('id') userId: bigint,
    @UploadedFile() file: { originalname: string; mimetype: string; buffer: Buffer; size: number } | undefined,
    @Body() body: { paymentMethodId: string; amount: string },
  ) {
    if (!file) throw appBadRequest('SCREENSHOT_REQUIRED');
    if (!file.mimetype.startsWith('image/')) throw appBadRequest('FILE_MUST_BE_IMAGE');

    const amount = parseFloat(body.amount);
    if (!amount || amount <= 0) throw appBadRequest('INVALID_AMOUNT');
    if (!body.paymentMethodId) throw appBadRequest('PAYMENT_METHOD_REQUIRED');

    // Save screenshot
    const ext = extname(file.originalname || '.jpg').toLowerCase() || '.jpg';
    const filename = `${Date.now()}-${randomUUID().slice(0, 8)}${ext}`;
    const root = getUploadRoot();
    const targetDir = join(root, 'deposits');
    await mkdir(targetDir, { recursive: true });
    await writeFile(join(targetDir, filename), file.buffer);
    const screenshotUrl = `/uploads/deposits/${filename}`;

    const order = await this.deposit.createDepositOrder(
      userId,
      BigInt(body.paymentMethodId),
      amount,
      screenshotUrl,
    );

    return jsonResponse({
      id: order.id.toString(),
      orderNo: order.orderNo,
      amount: order.amount.toString(),
      status: order.status,
      createdAt: order.createdAt,
    });
  }

  @Get('deposit-orders')
  async myDepositOrders(
    @CurrentUser('id') userId: bigint,
    @Query('page') page?: string,
  ) {
    const result = await this.deposit.getPlayerDepositOrders(
      userId,
      page ? parseInt(page, 10) : 1,
    );
    return jsonResponse(result);
  }

  @Get('deposit-orders/:id/audit-logs')
  async myDepositOrderAuditLogs(
    @CurrentUser('id') userId: bigint,
    @Param('id') id: string,
  ) {
    const items = await this.deposit.getPlayerDepositOrderAuditLogs(BigInt(id), userId);
    return jsonResponse({ items });
  }

  @Post('deposit-orders/:id/reapply')
  @UseInterceptors(FileInterceptor('screenshot', { limits: { fileSize: 5 * 1024 * 1024 } }))
  async reapplyDepositOrder(
    @CurrentUser('id') userId: bigint,
    @Param('id') id: string,
    @UploadedFile() file: { originalname: string; mimetype: string; buffer: Buffer; size: number } | undefined,
    @Body() body: { paymentMethodId?: string; amount?: string },
  ) {
    if (!file) throw appBadRequest('SCREENSHOT_REQUIRED');
    if (!file.mimetype.startsWith('image/')) throw appBadRequest('FILE_MUST_BE_IMAGE');

    const amount = body.amount != null && body.amount !== '' ? parseFloat(body.amount) : undefined;
    if (amount != null && (!amount || amount <= 0)) throw appBadRequest('INVALID_AMOUNT');

    const ext = extname(file.originalname || '.jpg').toLowerCase() || '.jpg';
    const filename = `${Date.now()}-${randomUUID().slice(0, 8)}${ext}`;
    const root = getUploadRoot();
    const targetDir = join(root, 'deposits');
    await mkdir(targetDir, { recursive: true });
    await writeFile(join(targetDir, filename), file.buffer);
    const screenshotUrl = `/uploads/deposits/${filename}`;

    const order = await this.deposit.reapplyDepositOrder(
      userId,
      BigInt(id),
      screenshotUrl,
      amount,
      body.paymentMethodId ? BigInt(body.paymentMethodId) : undefined,
    );

    return jsonResponse({
      id: order!.id.toString(),
      orderNo: order!.orderNo,
      amount: order!.amount.toString(),
      status: order!.status,
      createdAt: order!.createdAt,
    });
  }

  // ============ Messages / Inbox ============

  @Get('messages')
  async listMessages(
    @CurrentUser('id') userId: bigint,
    @Query('page') page?: string,
    @Query('pageSize') pageSize?: string,
  ) {
    const result = await this.playerMessages.listForPlayer(
      userId,
      page ? parseInt(page, 10) : 1,
      pageSize ? parseInt(pageSize, 10) : 20,
    );
    return jsonResponse(result);
  }

  @Get('messages/unread-count')
  async messageUnreadCount(@CurrentUser('id') userId: bigint) {
    const result = await this.playerMessages.getUnreadCount(userId);
    return jsonResponse(result);
  }

  @Get('messages/:id')
  async messageDetail(@CurrentUser('id') userId: bigint, @Param('id') id: string) {
    const message = await this.playerMessages.getForPlayer(userId, BigInt(id));
    return jsonResponse(message);
  }

  @Patch('messages/read-all')
  async markAllMessagesRead(@CurrentUser('id') userId: bigint) {
    const result = await this.playerMessages.markAllRead(userId);
    return jsonResponse(result);
  }

  @Patch('messages/:id/read')
  async markMessageRead(@CurrentUser('id') userId: bigint, @Param('id') id: string) {
    const message = await this.playerMessages.markRead(userId, BigInt(id));
    return jsonResponse(message);
  }

  @Delete('messages')
  async deleteAllMessages(@CurrentUser('id') userId: bigint) {
    const result = await this.playerMessages.deleteAllForPlayer(userId);
    return jsonResponse(result);
  }

  @Delete('messages/:id')
  async deleteMessage(@CurrentUser('id') userId: bigint, @Param('id') id: string) {
    const result = await this.playerMessages.deleteForPlayer(userId, BigInt(id));
    return jsonResponse(result);
  }
}
