import {
  Injectable,
  HttpStatus,
  BadRequestException,
  NotFoundException,
  ConflictException,
  InternalServerErrorException,
} from '@nestjs/common';
import { Kysely, sql } from 'kysely';
import { DB } from '@/database/database.type';
import { Logger } from 'pino-nestjs';
import { ReportContentDto, type GrantCreditsPurchaseInput } from './users.dto';
import { WELCOME_CREDITS, REWARDED_AD_CREDITS } from '@/config/constants';
import { WalletService } from '../wallet/wallet.service';
import { ErrorsService } from '../errors/errors.service';

@Injectable()
export class UsersService {
  constructor(
    private readonly db: Kysely<DB>,
    private readonly logger: Logger,
    private readonly walletService: WalletService,
    private readonly errorsService: ErrorsService,
  ) {}

  /**
   * Update user push token
   */
  async updatePushToken(user_id: string, token: string) {
    if (!token) {
      throw new BadRequestException('Token is required');
    }

    const result = await this.db
      .updateTable('users')
      .set({ push_token: token })
      .where('id', '=', user_id)
      .where('deleted_at', 'is', null)
      .returning('id')
      .executeTakeFirst();

    if (!result) {
      throw new NotFoundException('User not found');
    }

    return {
      status: true,
      statusCode: HttpStatus.OK,
      data: { message: 'Push token updated' },
    };
  }

  /**
   * Get list of blocked users
   */
  async getBlockedUsers(user_id: string) {
    const result = await this.db
      .selectFrom('users as u')
      .innerJoin('blocked_users as b', 'u.id', 'b.blocked_id')
      .select(['u.id', 'u.display_name', 'u.email', 'b.created_at as blockedAt'])
      .where('b.blocker_id', '=', user_id)
      .where('b.deleted_at', 'is', null)
      .where('u.deleted_at', 'is', null)
      .orderBy('b.created_at', 'desc')
      .execute();

    return {
      status: true,
      statusCode: HttpStatus.OK,
      data: result,
    };
  }

  /**
   * Block a user
   */
  async blockUser(user_id: string, blockedUserId: string) {
    if (user_id === blockedUserId) {
      throw new BadRequestException('You cannot block yourself');
    }

    const blockedUser = await this.db
      .selectFrom('users')
      .select('id')
      .where('id', '=', blockedUserId)
      .where('deleted_at', 'is', null)
      .executeTakeFirst();

    if (!blockedUser) {
      throw new NotFoundException('User not found');
    }

    try {
      const existing = await this.db
        .selectFrom('blocked_users')
        .select(['id', 'deleted_at'])
        .where('blocker_id', '=', user_id)
        .where('blocked_id', '=', blockedUserId)
        .executeTakeFirst();

      if (existing) {
        if (!existing.deleted_at) {
          throw new ConflictException('User already blocked');
        }

        await this.db
          .updateTable('blocked_users')
          .set({ deleted_at: null, created_at: new Date() })
          .where('id', '=', existing.id)
          .execute();
      } else {
        await this.db
          .insertInto('blocked_users')
          .values({
            blocker_id: user_id,
            blocked_id: blockedUserId,
          })
          .execute();
      }
    } catch (e: any) {
      this.logger.error('Error while blocking user', e);
      throw e;
    }

    return {
      status: true,
      statusCode: HttpStatus.OK,
      data: { message: 'User blocked' },
    };
  }

  /**
   * Unblock a user
   */
  async unblockUser(user_id: string, blockedUserId: string) {
    const result = await this.db
      .updateTable('blocked_users')
      .set({ deleted_at: new Date() })
      .where('blocker_id', '=', user_id)
      .where('blocked_id', '=', blockedUserId)
      .where('deleted_at', 'is', null)
      .returning('id')
      .executeTakeFirst();

    if (!result) {
      throw new NotFoundException('Block record not found');
    }

    return {
      status: true,
      statusCode: HttpStatus.OK,
      data: { message: 'User unblocked' },
    };
  }

  /**
   * Report content or user
   */
  async reportContent(user_id: string, data: ReportContentDto) {
    const { targetType, targetId, reason, description } = data;

    const report_id = targetType === 'report' ? targetId : null;
    const commentId = targetType === 'comment' ? targetId : null;
    const details =
      targetType === 'user' || targetType === 'story' || targetType === 'quick_snap'
        ? `targetType=${targetType}; targetId=${targetId}; ${description || ''}`.trim()
        : description || null;

    // TODO: need to check pet_report record exist ??
    const inserted = await this.db
      .insertInto('content_reports')
      .values({
        reporter_id: user_id,
        report_id: report_id,
        comment_id: commentId,
        reason: reason,
        details: details,
        status: 'pending',
      })
      .returning('id')
      .executeTakeFirstOrThrow();

    void this.errorsService.hookLogger({
      event: 'content_report_submitted',
      contentReportId: inserted.id,
      targetType,
      targetId,
      reason,
    });

    return {
      status: true,
      statusCode: HttpStatus.CREATED,
      data: { message: 'Report submitted' },
    };
  }

  /**
   * Get user credit balance
   */
  async getUserCreditBalance(user_id: string, client: Kysely<DB> = this.db) {
    const result = await client
      .selectFrom('users')
      .select([
        sql<number>`COALESCE(basic_credits, 0)::int`.as('basic_credits'),
        sql<number>`COALESCE(additional_credits, 0)::int`.as('additional_credits'),
      ])
      .where('id', '=', user_id)
      .where('deleted_at', 'is', null)
      .executeTakeFirst();

    if (!result) {
      throw new NotFoundException('User not found');
    }

    const basicCredits = Number(result.basic_credits || 0);
    const additionalCredits = Number(result.additional_credits || 0);
    return {
      status: true,
      statusCode: HttpStatus.OK,
      data: {
        basic_credits: basicCredits,
        additional_credits: additionalCredits,
        total_credits: basicCredits + additionalCredits,
      },
    };
  }

  private resolvePayloadEnvironment(storePayload?: Record<string, unknown>): string {
    const root = storePayload && typeof storePayload === 'object' ? storePayload : {};
    const payload =
      root.payload && typeof root.payload === 'object' && !Array.isArray(root.payload)
        ? (root.payload as Record<string, unknown>)
        : root;

    const value = payload.environmentIOS || payload.environment || root.environmentIOS || root.environment;
    const parsed = value ? String(value).trim() : '';
    return parsed || 'unknown';
  }

  /**
   * Grant additional credits for purchase
   */
  async grantAdditionalCreditsForPurchase(input: GrantCreditsPurchaseInput) {
    const {
      user_id,
      productId,
      platform,
      credits,
      priceUsd,
      currency = 'USD',
      transactionId,
      originalTransactionId,
      purchaseToken,
      storePayload = {},
    } = input;

    if (!user_id || !productId || !platform || !Number.isFinite(credits) || credits <= 0) {
      throw new BadRequestException('Invalid credit purchase payload');
    }

    const environment = this.resolvePayloadEnvironment(storePayload);

    const runInTransaction = async (trx: Kysely<DB>) => {
      const logResult = await trx
        .insertInto('credit_purchase_logs')
        .values({
          user_id,
          platform,
          environment,
          product_id: productId,
          credits_amount: credits,
          price_usd: priceUsd != null ? String(priceUsd) : null,
          currency,
          transaction_id: transactionId || null,
          original_transaction_id: originalTransactionId || null,
          purchase_token: purchaseToken || null,
          store_payload: JSON.stringify(storePayload || {}),
        })
        .onConflict((oc) =>
          oc
            .columns(['platform', 'environment', 'transaction_id'])
            .where('transaction_id', 'is not', null)
            .where('deleted_at', 'is', null)
            .doNothing(),
        )
        .returning('id')
        .execute();

      if (logResult.length === 0) {
        const current = await this.getUserCreditBalance(user_id, trx);
        return {
          status: true,
          statusCode: HttpStatus.OK,
          data: {
            duplicate: true,
            credits_added: 0,
            balance: current.status ? current.data : null,
          },
        };
      }

      const updated = await trx
        .updateTable('users')
        .set({
          additional_credits: sql`COALESCE(additional_credits, 0) + ${credits}`,
          updated_at: new Date(),
        })
        .where('id', '=', user_id)
        .where('deleted_at', 'is', null)
        .returning([
          sql<number>`COALESCE(basic_credits, 0)::int`.as('basic_credits'),
          sql<number>`COALESCE(additional_credits, 0)::int`.as('additional_credits'),
        ])
        .executeTakeFirst();

      if (!updated) {
        throw new NotFoundException('User not found');
      }

      const basicCredits = Number(updated.basic_credits || 0);
      const additionalCredits = Number(updated.additional_credits || 0);
      return {
        status: true,
        statusCode: HttpStatus.OK,
        data: {
          duplicate: false,
          credits_added: credits,
          balance: {
            basic_credits: basicCredits,
            additional_credits: additionalCredits,
            total_credits: basicCredits + additionalCredits,
          },
        },
      };
    };

    if (input.client) {
      return runInTransaction(input.client);
    } else {
      return this.db.transaction().execute(async (trx) => {
        return runInTransaction(trx);
      });
    }
  }

  /**
   * Deduct user credits
   */
  async deductUserCredits(userId: string, creditsToUse: number = 1, client?: Kysely<DB>) {
    if (!Number.isInteger(creditsToUse) || creditsToUse <= 0) {
      throw new BadRequestException('Invalid credit usage request');
    }

    const runInTransaction = async (trx: Kysely<DB>) => {
      const existing = await trx
        .selectFrom('users')
        .select([
          sql<number>`COALESCE(basic_credits, 0)::int`.as('basic_credits'),
          sql<number>`COALESCE(additional_credits, 0)::int`.as('additional_credits'),
        ])
        .where('id', '=', userId)
        .where('deleted_at', 'is', null)
        .modifyEnd(sql`FOR UPDATE`) // TODO: later implement cache lock instead of db row lock. DB LOCK = DOOM
        .executeTakeFirst();

      if (!existing) {
        throw new NotFoundException('User not found');
      }

      const basicCredits = Number(existing.basic_credits || 0);
      const additionalCredits = Number(existing.additional_credits || 0);
      const total = basicCredits + additionalCredits;

      if (total < creditsToUse) {
        throw new BadRequestException('Not enough credits. Please purchase more credits.');
      }

      const deductFromBasic = Math.min(basicCredits, creditsToUse);
      const remainingAfterBasic = creditsToUse - deductFromBasic;
      const deductFromAdditional = Math.min(additionalCredits, remainingAfterBasic);

      const nextBasic = basicCredits - deductFromBasic;
      const nextAdditional = additionalCredits - deductFromAdditional;

      await trx
        .updateTable('users')
        .set({
          basic_credits: nextBasic,
          additional_credits: nextAdditional,
          updated_at: sql`now()`,
        })
        .where('id', '=', userId)
        .where('deleted_at', 'is', null)
        .execute();

      return {
        status: true,
        statusCode: HttpStatus.OK,
        data: {
          credits_used: creditsToUse,
          deducted_from_basic: deductFromBasic,
          deducted_from_additional: deductFromAdditional,
          basic_credits: nextBasic,
          additional_credits: nextAdditional,
          total_credits: nextBasic + nextAdditional,
        },
      };
    };
    if (client) {
      return runInTransaction(client);
    } else {
      return this.db.transaction().execute(async (trx) => {
        return runInTransaction(trx);
      });
    }
  }

  /**
   * Permanently deletes the account and user-owned content (hard delete, not
   * a soft `deleted_at` flag) — matches Express exactly. Most tables cascade
   * or SET NULL on `users.id` automatically; `claim_requests` has a
   * restrictive FK and must be cleared first, and `devices` is explicitly
   * purged for privacy (push tokens + last-known location) even though its
   * FK would otherwise just SET NULL. Financial ledgers (credit_ledger,
   * purchase_events, etc.) intentionally SET NULL so audit records survive
   * without retaining a link to the deleted account.
   */
  async deleteAccount(userId: string) {
    return this.db.transaction().execute(async (trx) => {
      const existing = await trx
        .selectFrom('users')
        .select('id')
        .where('id', '=', userId)
        .where('deleted_at', 'is', null)
        .forUpdate()
        .executeTakeFirst();

      if (!existing) {
        throw new NotFoundException('Account not found');
      }

      await sql`
        DELETE FROM claim_requests
        WHERE claimer_user_id = ${userId}
           OR found_user_id = ${userId}
           OR lost_report_id IN (SELECT id FROM pet_reports WHERE user_id = ${userId})
           OR found_report_id IN (SELECT id FROM pet_reports WHERE user_id = ${userId})
      `.execute(trx);

      await trx.deleteFrom('devices').where('user_id', '=', userId).execute();

      const deleted = await trx.deleteFrom('users').where('id', '=', userId).returning('id').executeTakeFirst();

      if (!deleted) {
        throw new InternalServerErrorException('Account deletion did not remove the user');
      }

      return {
        status: true,
        statusCode: HttpStatus.OK,
        data: { message: 'Account and associated data deleted' },
      };
    });
  }

  async getCreditActivity(userId: string, page = 0, limit = 50) {
    // Credit activity is the ledger (credit_ledger), owned by WalletService — mirrors Express.
    return this.walletService.getCreditActivity(userId, page, limit);
  }

  /**
   * Grant rewarded ad credits
   */
  async grantRewardAdCredits(userId: string) {
    if (!userId) {
      throw new BadRequestException('User not found');
    }

    const updated = await this.db
      .updateTable('users')
      .set({
        additional_credits: sql`COALESCE(additional_credits, 0) + ${REWARDED_AD_CREDITS}`,
        updated_at: sql`now()`,
      })
      .where('id', '=', userId)
      .where('deleted_at', 'is', null)
      .returning([
        sql<number>`COALESCE(basic_credits, 0)::int`.as('basic_credits'),
        sql<number>`COALESCE(additional_credits, 0)::int`.as('additional_credits'),
      ])
      .executeTakeFirst();

    if (!updated) {
      throw new NotFoundException('User not found');
    }

    const basicCredits = Number(updated.basic_credits || 0);
    const additionalCredits = Number(updated.additional_credits || 0);

    return {
      status: true,
      statusCode: HttpStatus.OK,
      data: {
        credits_added: REWARDED_AD_CREDITS,
        balance: {
          basic_credits: basicCredits,
          additional_credits: additionalCredits,
          total_credits: basicCredits + additionalCredits,
        },
      },
    };
  }

  /**
   * Lightweight referral summary for the nested `GET /users/referral` route.
   * Read-only snapshot (no validation/reward processing — that lives in the
   * top-level `/referral` module).
   */
  async getReferralStats(userId: string) {
    const userResult = await sql<{ referral_code: string | null; premium_until: string | null }>`
      SELECT referral_code, premium_until FROM users WHERE id = ${userId} AND deleted_at IS NULL
    `.execute(this.db);

    if (userResult.rows.length === 0) {
      throw new NotFoundException('User not found');
    }

    const referralCode = userResult.rows[0].referral_code || '';
    const premiumUntil = userResult.rows[0].premium_until;

    const referralCount = await sql<{ count: number }>`
      SELECT COUNT(*)::int AS count FROM referral_attributions
      WHERE referrer_user_id = ${userId} AND status IN ('VALID', 'REWARDED') AND deleted_at IS NULL
    `.execute(this.db);

    const rewardsResult = await sql<{
      type: string;
      days_awarded: number;
      reason: string;
      created_at: Date;
    }>`
      SELECT CASE
               WHEN reward_type = 'ambassador' THEN 'ambassador'
               WHEN reward_type LIKE 'referral_%' THEN 'referral'
               ELSE reward_type
             END AS type,
             granted_days AS days_awarded, reason, granted_at AS created_at
      FROM reward_grants
      WHERE user_id = ${userId} AND status = 'GRANTED' AND deleted_at IS NULL
      ORDER BY granted_at DESC
    `.execute(this.db);

    const now = new Date();
    const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1);
    const sharesResult = await sql<{ platform: string; shared_date: string }>`
      SELECT platform, shared_date FROM social_shares
      WHERE user_id = ${userId} AND shared_date >= ${startOfMonth.toISOString()}
        AND status IN ('VALID', 'REWARDED') AND COALESCE(share_intent_token, '') <> ''
        AND COALESCE(share_duration_ms, 0) >= 1500 AND deleted_at IS NULL
      ORDER BY shared_date DESC
    `.execute(this.db);

    return {
      status: true,
      statusCode: HttpStatus.OK,
      data: {
        referralCode,
        referralCount: Number(referralCount.rows[0]?.count || 0),
        premiumUntil: premiumUntil ? new Date(premiumUntil).toISOString() : null,
        rewards: rewardsResult.rows.map((r) => ({
          type: r.type,
          daysAwarded: r.days_awarded,
          reason: r.reason,
          created_at: new Date(r.created_at).toISOString(),
        })),
        sharesThisMonth: sharesResult.rows.length,
        shares: sharesResult.rows.map((s) => ({ platform: s.platform, date: s.shared_date })),
      },
    };
  }
}
