import {
  Injectable,
  BadRequestException,
  ForbiddenException,
  NotFoundException,
  HttpException,
  HttpStatus,
} from '@nestjs/common';
import { Kysely, sql } from 'kysely';
import { DB } from '@/database/database.type';
import { Logger } from 'pino-nestjs';
import { createHash, randomUUID } from 'crypto';
import { WalletService } from '../wallet/wallet.service';

type AdToggles = {
  googleAdsEnabled: boolean;
  ownAdsEnabled: boolean;
  rewardedAdsEnabled: boolean;
};

type RewardType = 'credits' | 'found_report_bonus';
type RewardAdNetwork = 'admob' | 'custom';

function parseBoolean(value: unknown, fallback = false): boolean {
  if (typeof value === 'boolean') return value;
  if (typeof value === 'string') {
    const normalized = value.trim().toLowerCase();
    if (['true', '1', 'yes', 'on'].includes(normalized)) return true;
    if (['false', '0', 'no', 'off'].includes(normalized)) return false;
  }
  return fallback;
}

function getRewardValue(type: RewardType): number {
  return type === 'credits' ? 3 : 1;
}

function normalizeRewardType(input?: unknown): RewardType {
  return input === 'found_report_bonus' ? 'found_report_bonus' : 'credits';
}

function clampWatchSeconds(value: unknown): number {
  const parsed = Number(value);
  if (!Number.isFinite(parsed)) return 15;
  return Math.min(120, Math.max(5, Math.floor(parsed)));
}

function parseVerificationPayload(raw: unknown): Record<string, any> {
  if (!raw) return {};
  if (typeof raw === 'object' && !Array.isArray(raw)) return raw as Record<string, any>;
  if (typeof raw === 'string') {
    try {
      const parsed = JSON.parse(raw);
      if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) return parsed;
    } catch {
      return {};
    }
  }
  return {};
}

function normalizeParam(value: unknown): string {
  if (typeof value === 'string') return value.trim();
  if (Array.isArray(value) && value[0]) return String(value[0]).trim();
  return '';
}

function buildVerificationHash(payload: Record<string, unknown>, rawQuery: string): string {
  const source = rawQuery.trim() || JSON.stringify(payload || {});
  return createHash('sha256').update(source).digest('hex');
}

function parsePhotoUris(value: unknown): string[] {
  if (typeof value === 'string') {
    try {
      return JSON.parse(value);
    } catch {
      return [];
    }
  }
  return (value as string[]) || [];
}

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

  private async getAdToggles(): Promise<AdToggles> {
    const rows = await this.db
      .selectFrom('app_settings')
      .select(['key', 'value'])
      .where('key', 'in', [
        'ads_enabled',
        'google_ads_enabled',
        'own_ads_enabled',
        'feature_rewarded_ads_enabled',
      ])
      .where('deleted_at', 'is', null)
      .execute();

    const map = new Map<string, string>();
    for (const row of rows) map.set(String(row.key), String(row.value));

    const legacyAdsEnabled = parseBoolean(map.get('ads_enabled'), true);
    const googleAdsEnabled = parseBoolean(map.get('google_ads_enabled'), true) && legacyAdsEnabled;
    const ownAdsEnabled = parseBoolean(map.get('own_ads_enabled'), true) && legacyAdsEnabled;
    const rewardedAdsEnabled =
      parseBoolean(map.get('feature_rewarded_ads_enabled'), true) &&
      (googleAdsEnabled || ownAdsEnabled);

    return { googleAdsEnabled, ownAdsEnabled, rewardedAdsEnabled };
  }

  async getActiveAds() {
    const { ownAdsEnabled } = await this.getAdToggles();
    if (!ownAdsEnabled) {
      return { status: true, statusCode: 200, data: [] };
    }

    const result = await sql<any>`
      SELECT * FROM ads
      WHERE status = 'approved'
        AND deleted_at IS NULL
        AND (end_date IS NULL OR end_date > NOW())
      ORDER BY RANDOM()
      LIMIT 5
    `.execute(this.db);

    return {
      status: true,
      statusCode: 200,
      data: result.rows.map((ad: any) => ({
        ...ad,
        logo_uri: ad.logo_uri || null,
        ad_image_uri: ad.ad_image_uri || ad.image_uri || null,
        link_type: ad.link_type || 'website',
        photo_uris: parsePhotoUris(ad.photo_uris),
      })),
    };
  }

  private async getRandomActiveCustomRewardAd(rewardType: RewardType) {
    const result = await sql<any>`
      SELECT id, business_name, business_type, logo_uri, ad_image_uri, image_uri, video_url, link_url, link_type, description, website, reward_value, reward_watch_seconds
      FROM ads
      WHERE status = 'approved'
        AND deleted_at IS NULL
        AND COALESCE(is_reward_enabled, false) = true
        AND COALESCE(reward_type, 'credits') = ${rewardType}
        AND (start_date IS NULL OR start_date <= NOW())
        AND (end_date IS NULL OR end_date > NOW())
      ORDER BY RANDOM()
      LIMIT 1
    `.execute(this.db);

    return result.rows[0] || null;
  }

  async startRewardIntent(
    user_id: string,
    input: { rewardType?: RewardType; adUnitId?: string; adNetwork?: RewardAdNetwork },
  ) {
    const { rewardedAdsEnabled, ownAdsEnabled } = await this.getAdToggles();
    if (!rewardedAdsEnabled) {
      throw new ForbiddenException('Rewarded ads are currently disabled');
    }

    const rewardType = normalizeRewardType(input.rewardType);
    const adNetwork: RewardAdNetwork = input.adNetwork === 'custom' ? 'custom' : 'admob';
    if (!user_id) {
      throw new BadRequestException('User not found');
    }

    const intentToken = randomUUID();
    const expiresAt = new Date(Date.now() + 30 * 60 * 1000);
    let rewardValue = getRewardValue(rewardType);
    let verificationPayload: Record<string, unknown> = {};
    let adUnitOrSourceId: string | null = input.adUnitId || null;
    let responseExtras: Record<string, unknown> = {};

    if (adNetwork === 'custom') {
      if (!ownAdsEnabled) {
        throw new NotFoundException('No active custom reward ad available');
      }
      const customAd = await this.getRandomActiveCustomRewardAd(rewardType);
      if (!customAd) {
        throw new NotFoundException('No active custom reward ad available');
      }

      const watchSeconds = clampWatchSeconds(customAd.reward_watch_seconds);
      rewardValue = Math.max(1, Number(customAd.reward_value || rewardValue));
      adUnitOrSourceId = String(customAd.id);
      verificationPayload = { source: 'custom_ad', adId: customAd.id, minWatchSeconds: watchSeconds };
      responseExtras = {
        network: 'custom',
        minWatchSeconds: watchSeconds,
        claimAvailableAt: new Date(Date.now() + watchSeconds * 1000).toISOString(),
        ad: {
          id: customAd.id,
          business_name: customAd.business_name,
          business_type: customAd.business_type,
          logo_uri: customAd.logo_uri,
          ad_image_uri: customAd.ad_image_uri || customAd.image_uri,
          image_uri: customAd.image_uri,
          video_url: customAd.video_url,
          link_url: customAd.link_url,
          link_type: customAd.link_type || 'website',
          description: customAd.description,
          website: customAd.website,
        },
      };
    } else {
      responseExtras = { network: 'admob' };
    }

    await sql`
      INSERT INTO ad_reward_events (
        user_id, ad_network, ad_unit_id, reward_type, reward_value, intent_token, status, expires_at, verification_payload
      ) VALUES (${user_id}, ${adNetwork}, ${adUnitOrSourceId}, ${rewardType}, ${rewardValue}, ${intentToken}, 'pending', ${expiresAt.toISOString()}, ${JSON.stringify(verificationPayload || {})}::jsonb)
    `.execute(this.db);

    return {
      status: true,
      statusCode: 201,
      data: {
        intentToken,
        rewardType,
        rewardValue,
        expiresAt,
        ssvCustomData: intentToken,
        ...responseExtras,
      },
    };
  }

  async verifyAdmobReward(payload: Record<string, unknown>, rawQuery = '') {
    const { rewardedAdsEnabled } = await this.getAdToggles();
    if (!rewardedAdsEnabled) {
      return {
        status: true,
        statusCode: 200,
        data: { verified: false, disabled: true, message: 'Rewarded ads are disabled' },
      };
    }

    const intentToken = normalizeParam(payload.custom_data);
    const signature = normalizeParam(payload.signature);
    const keyId = normalizeParam(payload.key_id);
    const tokenFromQuery = normalizeParam(payload.token);
    const expectedToken = String(process.env.ADMOB_SSV_VERIFICATION_TOKEN || '').trim();

    // AdMob dashboard URL verification may call without reward fields.
    if (!intentToken && !signature && !keyId) {
      if (expectedToken && tokenFromQuery && tokenFromQuery !== expectedToken) {
        throw new HttpException(
          { status: false, statusCode: 401, message: 'Invalid AdMob verification token' },
          HttpStatus.UNAUTHORIZED,
        );
      }
      return {
        status: true,
        statusCode: 200,
        data: { verified: false, probe: true, message: 'AdMob SSV callback endpoint reachable' },
      };
    }

    if (expectedToken && tokenFromQuery && tokenFromQuery !== expectedToken) {
      throw new HttpException(
        { status: false, statusCode: 401, message: 'Invalid AdMob verification token' },
        HttpStatus.UNAUTHORIZED,
      );
    }

    if (!intentToken) {
      return {
        status: true,
        statusCode: 200,
        data: { verified: false, probe: true, message: 'Missing custom_data; callback acknowledged' },
      };
    }

    const verificationHash = buildVerificationHash(payload, rawQuery);
    const existingByHash = await sql<{ intent_token: string }>`
      SELECT id, intent_token, status FROM ad_reward_events
      WHERE verification_hash = ${verificationHash} AND deleted_at IS NULL LIMIT 1
    `.execute(this.db);
    if (existingByHash.rows.length > 0) {
      return {
        status: true,
        statusCode: 200,
        data: { duplicate: true, intentToken: String(existingByHash.rows[0].intent_token || '') },
      };
    }

    const updated = await sql<{ status: string }>`
      UPDATE ad_reward_events
      SET status = CASE
             WHEN status = 'claimed' THEN 'claimed'
             WHEN expires_at IS NOT NULL AND expires_at <= NOW() THEN 'expired'
             ELSE 'verified'
           END,
          verification_payload = ${JSON.stringify(payload || {})},
          verification_signature = ${signature},
          verification_key_id = ${keyId},
          verification_hash = ${verificationHash},
          verified_at = CASE WHEN status = 'claimed' THEN verified_at ELSE NOW() END,
          updated_at = NOW()
      WHERE intent_token = ${intentToken} AND ad_network = 'admob' AND deleted_at IS NULL
      RETURNING id, status, reward_type, reward_value, user_id
    `.execute(this.db);

    if (updated.rows.length === 0) {
      throw new NotFoundException('Reward intent not found');
    }

    return {
      status: true,
      statusCode: 200,
      data: {
        verified: updated.rows[0].status === 'verified' || updated.rows[0].status === 'claimed',
        status: updated.rows[0].status,
        intentToken,
      },
    };
  }

  async claimRewardIntent(user_id: string, intentToken: string) {
    const { rewardedAdsEnabled } = await this.getAdToggles();
    if (!rewardedAdsEnabled) {
      throw new ForbiddenException('Rewarded ads are currently disabled');
    }
    if (!user_id || !intentToken) {
      throw new BadRequestException('Invalid reward claim payload');
    }

    return this.db.transaction().execute(async (trx) => {
      const eventResult = await sql<any>`
        SELECT id, user_id, reward_type, reward_value, status, expires_at, ad_unit_id, ad_network, created_at, verification_payload
        FROM ad_reward_events
        WHERE intent_token = ${intentToken} AND deleted_at IS NULL
        FOR UPDATE
      `.execute(trx);

      if (eventResult.rows.length === 0) {
        throw new NotFoundException('Reward intent not found');
      }

      const event = eventResult.rows[0];
      if (String(event.user_id) !== user_id) {
        throw new ForbiddenException('Reward intent does not belong to user');
      }

      if (event.status === 'claimed') {
        return {
          status: true,
          statusCode: 200,
          data: { duplicate: true, rewardType: event.reward_type, message: 'Reward already claimed' },
        };
      }

      if (event.status !== 'verified') {
        const parsed = parseVerificationPayload(event.verification_payload);
        const minWatchSeconds = clampWatchSeconds(parsed.minWatchSeconds);
        const createdAtMs = new Date(event.created_at || 0).getTime();
        const eligibleAtMs = createdAtMs + minWatchSeconds * 1000;
        const isCustomWatchEligible =
          event.ad_network === 'custom' &&
          event.status === 'pending' &&
          Number.isFinite(createdAtMs) &&
          Date.now() >= eligibleAtMs;

        if (event.ad_network === 'custom' && event.status === 'pending' && !isCustomWatchEligible) {
          throw new BadRequestException('Minimum watch time not completed yet');
        }

        const isAdmobClaimEligible = event.status === 'pending' && event.ad_network === 'admob';

        if (isCustomWatchEligible) {
          await sql`
            UPDATE ad_reward_events
            SET status = 'verified', verified_at = NOW(),
                verification_payload = jsonb_set(coalesce(verification_payload, '{}'::jsonb), '{custom_watch_verified_at}', to_jsonb(NOW()::text), true),
                updated_at = NOW()
            WHERE id = ${event.id}
          `.execute(trx);
          event.status = 'verified';
        } else if (isAdmobClaimEligible) {
          await sql`
            UPDATE ad_reward_events
            SET status = 'verified', verified_at = NOW(),
                verification_payload = jsonb_set(coalesce(verification_payload, '{}'::jsonb), '{admob_ssv_bypassed_at}', to_jsonb(${new Date().toISOString()}::text), true),
                updated_at = NOW()
            WHERE id = ${event.id}
          `.execute(trx);
          event.status = 'verified';
        } else {
          throw new BadRequestException('Reward intent is not verified yet');
        }
      }

      if (event.expires_at && new Date(event.expires_at) <= new Date()) {
        await sql`UPDATE ad_reward_events SET status = 'expired', updated_at = NOW() WHERE id = ${event.id}`.execute(
          trx,
        );
        throw new BadRequestException('Reward intent expired');
      }

      let responseData: Record<string, unknown> = {
        duplicate: false,
        rewardType: String(event.reward_type),
      };

      if (event.reward_type === 'credits') {
        const creditDelta = Number(event.reward_value || 3);
        const walletResult = await this.walletService.applyCreditMutation({
          client: trx,
          user_id,
          delta: creditDelta,
          reason: 'ad_reward_credit',
          referenceType: 'ad_reward',
          referenceId: String(event.id),
          idempotencyKey: `ad_reward_claim:${intentToken}`,
          metadata: { eventId: event.id, rewardType: event.reward_type },
        });
        if (!walletResult.status) {
          // Surface the wallet failure verbatim (e.g. insufficient credits).
          throw new HttpException(walletResult, walletResult.statusCode);
        }
        responseData = {
          ...responseData,
          credits_added: creditDelta,
          balance: walletResult.data?.balance || null,
        };
      } else {
        const bonusToken = randomUUID();
        const expiresAt = new Date(Date.now() + 24 * 60 * 60 * 1000);
        await sql`
          INSERT INTO found_report_bonus_tokens (user_id, source_event_id, token, status, expires_at)
          VALUES (${user_id}, ${event.id}, ${bonusToken}, 'issued', ${expiresAt.toISOString()})
        `.execute(trx);
        responseData = { ...responseData, found_bonus_token: bonusToken, expiresAt };
      }

      await sql`
        UPDATE ad_reward_events SET status = 'claimed', claimed_at = NOW(), updated_at = NOW() WHERE id = ${event.id}
      `.execute(trx);

      return { status: true, statusCode: 200, data: responseData };
    });
  }
}
