import {
  Injectable,
  BadRequestException,
  NotFoundException,
  ForbiddenException,
  ConflictException,
} from '@nestjs/common';
import { Kysely, sql } from 'kysely';
import { DB } from '@/database/database.type';
import { Logger } from 'pino-nestjs';
import {
  containsDisallowedUserContent,
  MODERATION_REJECTION_MESSAGE,
} from '@/utils/content-moderation';

type OpeningType = 'open_24h' | 'scheduled' | 'appointment' | 'unknown';

function inferOpeningType(hours?: string | null): OpeningType {
  const normalized = String(hours || '')
    .trim()
    .toLowerCase();
  if (!normalized) return 'unknown';
  if (normalized.includes('24') && normalized.includes('hour')) return 'open_24h';
  if (normalized.includes('appointment')) return 'appointment';
  return 'scheduled';
}

function parseFiniteNumber(value: unknown): number | null {
  if (value === null || value === undefined || value === '') return null;
  const parsed = Number(value);
  return Number.isFinite(parsed) ? parsed : null;
}

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

function haversineKm(lat1: number, lng1: number, lat2: number, lng2: number): number {
  const R = 6371;
  const dLat = ((lat2 - lat1) * Math.PI) / 180;
  const dLng = ((lng2 - lng1) * Math.PI) / 180;
  const a =
    Math.sin(dLat / 2) ** 2 +
    Math.cos((lat1 * Math.PI) / 180) *
      Math.cos((lat2 * Math.PI) / 180) *
      Math.sin(dLng / 2) ** 2;
  return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
}

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

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

  /** All approved public organisations, with optional text/type/geo filters applied in JS. */
  async getPublicOrganisations(query: Record<string, unknown> = {}) {
    const result = await sql<any>`
      SELECT o.*, COUNT(oa.id)::int AS animal_count
      FROM organisations o
      LEFT JOIN organisation_animals oa ON oa.org_id = o.id AND oa.status = 'available' AND oa.deleted_at IS NULL
      WHERE o.status = 'approved' AND o.deleted_at IS NULL
      GROUP BY o.id
      ORDER BY o.name
    `.execute(this.db);

    const needle = parseString(query.q).trim().toLowerCase();
    const typeFilter = parseString(query.type).trim().toLowerCase();
    const openingTypeFilter = parseString(query.opening_type).trim().toLowerCase();
    const latitude = parseFiniteNumber(query.latitude);
    const longitude = parseFiniteNumber(query.longitude);
    const radiusKm = parseFiniteNumber(query.radius_km);
    const hasDistanceFilter =
      latitude !== null && longitude !== null && radiusKm !== null && radiusKm >= 0;

    const rows = result.rows
      .map((o: any) => {
        const openingHours = o.opening_hours || null;
        const openingType: OpeningType = o.opening_type || inferOpeningType(openingHours);

        const lat = Number(o.latitude);
        const lng = Number(o.longitude);
        const hasCoordinates = Number.isFinite(lat) && Number.isFinite(lng);
        const distanceKm =
          hasDistanceFilter && hasCoordinates
            ? haversineKm(latitude!, longitude!, lat, lng)
            : null;

        return {
          id: o.id,
          user_id: o.user_id,
          name: o.name,
          type: o.type,
          abn: o.abn,
          address: o.address,
          phone: o.phone,
          email: o.email,
          website: o.website,
          latitude: o.latitude,
          longitude: o.longitude,
          description: o.description,
          logo_uri: o.logo_uri,
          status: o.status,
          approved_at: o.approved_at,
          created_at: o.created_at,
          animal_count: o.animal_count,
          opening_type: openingType,
          opening_hours: openingHours,
          distance_km: distanceKm,
        };
      })
      .filter((o: any) => {
        if (typeFilter && !['vet', 'shelter', 'rescue'].includes(typeFilter)) return false;
        if (typeFilter && o.type !== typeFilter) return false;

        if (
          openingTypeFilter &&
          !['open_24h', 'scheduled', 'appointment', 'unknown'].includes(openingTypeFilter)
        ) {
          return false;
        }
        if (openingTypeFilter && o.opening_type !== openingTypeFilter) return false;

        if (needle) {
          const haystack = `${o.name || ''} ${o.address || ''}`.toLowerCase();
          if (!haystack.includes(needle)) return false;
        }

        if (hasDistanceFilter) {
          if (typeof o.distance_km !== 'number') return false;
          if (o.distance_km > radiusKm!) return false;
        }

        return true;
      })
      .sort((a: any, b: any) => {
        if (hasDistanceFilter) return a.distance_km - b.distance_km;
        return String(a.name || '').localeCompare(String(b.name || ''));
      });

    return { status: true, statusCode: 200, data: rows };
  }

  async getOrganisation(orgId: string) {
    const result = await sql<any>`
      SELECT * FROM organisations WHERE id = ${orgId} AND deleted_at IS NULL
    `.execute(this.db);

    if (result.rows.length === 0) {
      throw new NotFoundException('Organisation not found');
    }
    return { status: true, statusCode: 200, data: result.rows[0] };
  }

  async getOrganisationAnimals(orgId: string) {
    const result = await sql<any>`
      SELECT * FROM organisation_animals
      WHERE org_id = ${orgId} AND status = 'available' AND deleted_at IS NULL
      ORDER BY created_at DESC
    `.execute(this.db);

    return {
      status: true,
      statusCode: 200,
      data: result.rows.map((a: any) => ({
        id: a.id,
        orgId: a.org_id,
        pet_type: a.pet_type,
        pet_name: a.pet_name,
        breed: a.breed,
        size: a.size,
        color: a.color,
        markings: a.markings,
        photo_uris: parsePhotoUris(a.photo_uris),
        description: a.description,
        intake_date: a.intake_date,
        intake_type: a.intake_type,
        microchip_number: a.microchip_number,
        desexed: a.desexed,
        status: a.status,
        created_at: a.created_at,
        updatedAt: a.updated_at,
      })),
    };
  }

  async registerPartner(user_id: string, data: any) {
    const { name, type, abn, address, phone, email, website, latitude, longitude, description, logo_uri } =
      data;

    if (!name || !type || !address || !phone || !email) {
      throw new BadRequestException('Name, type, address, phone, and email are required');
    }

    if (containsDisallowedUserContent([name, address, website, description])) {
      throw new BadRequestException(MODERATION_REJECTION_MESSAGE);
    }

    if (!['vet', 'shelter', 'rescue'].includes(type)) {
      throw new BadRequestException('Type must be vet, shelter, or rescue');
    }

    const existing = await sql<{ id: string }>`
      SELECT id FROM organisations WHERE user_id = ${user_id} AND deleted_at IS NULL
    `.execute(this.db);
    if (existing.rows.length > 0) {
      throw new ConflictException('You already have a registered organisation');
    }

    const result = await sql<any>`
      INSERT INTO organisations (user_id, name, type, abn, address, phone, email, website, latitude, longitude, description, logo_uri)
      VALUES (${user_id}, ${name}, ${type}, ${abn || null}, ${address}, ${phone}, ${email}, ${website || null}, ${latitude || 0}, ${longitude || 0}, ${description || null}, ${logo_uri || null})
      RETURNING *
    `.execute(this.db);

    await sql`UPDATE users SET role = 'org' WHERE id = ${user_id} AND deleted_at IS NULL`.execute(
      this.db,
    );

    return { status: true, statusCode: 201, data: result.rows[0] };
  }

  async getMyOrganisation(user_id: string) {
    const result = await sql<any>`
      SELECT * FROM organisations WHERE user_id = ${user_id} AND deleted_at IS NULL
    `.execute(this.db);
    if (result.rows.length === 0) {
      return { status: true, statusCode: 200, data: null };
    }
    const org = result.rows[0];
    const animalCount = await sql<{ count: number }>`
      SELECT COUNT(*)::int AS count FROM organisation_animals WHERE org_id = ${org.id} AND deleted_at IS NULL
    `.execute(this.db);

    return {
      status: true,
      statusCode: 200,
      data: { ...org, animal_count: animalCount.rows[0].count },
    };
  }

  async updateMyOrganisation(user_id: string, data: any) {
    const { name, type, abn, address, phone, email, website, latitude, longitude, description, logo_uri } =
      data;

    if (containsDisallowedUserContent([name, address, website, description])) {
      throw new BadRequestException(MODERATION_REJECTION_MESSAGE);
    }

    const result = await sql<any>`
      UPDATE organisations
      SET name = COALESCE(${name ?? null}, name),
          type = COALESCE(${type ?? null}, type),
          abn = COALESCE(${abn ?? null}, abn),
          address = COALESCE(${address ?? null}, address),
          phone = COALESCE(${phone ?? null}, phone),
          email = COALESCE(${email ?? null}, email),
          website = COALESCE(${website ?? null}, website),
          latitude = COALESCE(${latitude ?? null}, latitude),
          longitude = COALESCE(${longitude ?? null}, longitude),
          description = COALESCE(${description ?? null}, description),
          logo_uri = COALESCE(${logo_uri ?? null}, logo_uri),
          updated_at = NOW()
      WHERE user_id = ${user_id} AND deleted_at IS NULL
      RETURNING *
    `.execute(this.db);

    if (result.rows.length === 0) {
      throw new NotFoundException('Organisation not found');
    }
    return { status: true, statusCode: 200, data: result.rows[0] };
  }

  async getMyAnimals(user_id: string) {
    const org = await sql<{ id: string }>`
      SELECT id FROM organisations WHERE user_id = ${user_id} AND deleted_at IS NULL
    `.execute(this.db);
    if (org.rows.length === 0) {
      return { status: true, statusCode: 200, data: [] };
    }
    const result = await sql<any>`
      SELECT * FROM organisation_animals WHERE org_id = ${org.rows[0].id} AND deleted_at IS NULL ORDER BY created_at DESC
    `.execute(this.db);
    return {
      status: true,
      statusCode: 200,
      data: result.rows.map((a: any) => ({ ...a, photo_uris: parsePhotoUris(a.photo_uris) })),
    };
  }

  async addAnimal(user_id: string, data: any) {
    const org = await sql<{ id: string; status: string }>`
      SELECT id, status FROM organisations WHERE user_id = ${user_id} AND deleted_at IS NULL
    `.execute(this.db);
    if (org.rows.length === 0) {
      throw new NotFoundException('No organisation found');
    }
    if (org.rows[0].status !== 'approved') {
      throw new ForbiddenException('Organisation must be approved to add animals');
    }
    const orgId = org.rows[0].id;
    const {
      pet_type,
      pet_name,
      breed,
      size,
      color,
      markings,
      photo_uris,
      description,
      intake_date,
      intake_type,
      microchip_number,
      desexed,
    } = data;

    if (!pet_type) {
      throw new BadRequestException('Pet type is required');
    }
    if (containsDisallowedUserContent([pet_name, breed, color, markings, description])) {
      throw new BadRequestException(MODERATION_REJECTION_MESSAGE);
    }
    const result = await sql<any>`
      INSERT INTO organisation_animals (org_id, pet_type, pet_name, breed, size, color, markings, photo_uris, description, intake_date, intake_type, microchip_number, desexed)
      VALUES (${orgId}, ${pet_type}, ${pet_name || ''}, ${breed || ''}, ${size || 'medium'}, ${color || ''}, ${markings || ''}, ${JSON.stringify(photo_uris || [])}, ${description || ''}, ${intake_date || null}, ${intake_type || 'stray'}, ${microchip_number || null}, ${desexed || false})
      RETURNING *
    `.execute(this.db);
    const a = result.rows[0];
    return { status: true, statusCode: 201, data: { ...a, photo_uris: parsePhotoUris(a.photo_uris) } };
  }

  async getMyAnimal(user_id: string, animalId: string) {
    const org = await sql<{ id: string }>`
      SELECT id FROM organisations WHERE user_id = ${user_id} AND deleted_at IS NULL
    `.execute(this.db);
    if (org.rows.length === 0) {
      throw new NotFoundException('No organisation found');
    }
    const result = await sql<any>`
      SELECT * FROM organisation_animals WHERE id = ${animalId} AND org_id = ${org.rows[0].id} AND deleted_at IS NULL
    `.execute(this.db);
    if (result.rows.length === 0) {
      throw new NotFoundException('Animal not found');
    }
    const a = result.rows[0];
    return { status: true, statusCode: 200, data: { ...a, photo_uris: parsePhotoUris(a.photo_uris) } };
  }

  async updateMyAnimal(user_id: string, animalId: string, data: any) {
    const org = await sql<{ id: string }>`
      SELECT id FROM organisations WHERE user_id = ${user_id} AND deleted_at IS NULL
    `.execute(this.db);
    if (org.rows.length === 0) {
      throw new NotFoundException('No organisation found');
    }
    const orgId = org.rows[0].id;
    const {
      pet_type,
      pet_name,
      breed,
      size,
      color,
      markings,
      photo_uris,
      description,
      intake_date,
      intake_type,
      microchip_number,
      desexed,
      status,
    } = data;

    if (containsDisallowedUserContent([pet_name, breed, color, markings, description])) {
      throw new BadRequestException(MODERATION_REJECTION_MESSAGE);
    }

    const result = await sql<any>`
      UPDATE organisation_animals
      SET pet_type = COALESCE(${pet_type ?? null}, pet_type),
          pet_name = COALESCE(${pet_name ?? null}, pet_name),
          breed = COALESCE(${breed ?? null}, breed),
          size = COALESCE(${size ?? null}, size),
          color = COALESCE(${color ?? null}, color),
          markings = COALESCE(${markings ?? null}, markings),
          photo_uris = COALESCE(${photo_uris ? JSON.stringify(photo_uris) : null}, photo_uris),
          description = COALESCE(${description ?? null}, description),
          intake_date = COALESCE(${intake_date ?? null}, intake_date),
          intake_type = COALESCE(${intake_type ?? null}, intake_type),
          microchip_number = COALESCE(${microchip_number ?? null}, microchip_number),
          desexed = COALESCE(${desexed ?? null}, desexed),
          status = COALESCE(${status ?? null}, status),
          updated_at = NOW()
      WHERE id = ${animalId} AND org_id = ${orgId} AND deleted_at IS NULL
      RETURNING *
    `.execute(this.db);

    if (result.rows.length === 0) {
      throw new NotFoundException('Animal not found or unauthorized');
    }
    const a = result.rows[0];
    return { status: true, statusCode: 200, data: { ...a, photo_uris: parsePhotoUris(a.photo_uris) } };
  }

  async deleteMyAnimal(user_id: string, animalId: string) {
    const org = await sql<{ id: string }>`
      SELECT id FROM organisations WHERE user_id = ${user_id} AND deleted_at IS NULL
    `.execute(this.db);
    if (org.rows.length === 0) {
      throw new NotFoundException('No organisation found');
    }
    const result = await sql<{ id: string }>`
      UPDATE organisation_animals SET deleted_at = NOW()
      WHERE id = ${animalId} AND org_id = ${org.rows[0].id} AND deleted_at IS NULL
      RETURNING id
    `.execute(this.db);
    if (result.rows.length === 0) {
      throw new NotFoundException('Animal not found or unauthorized');
    }
    return { status: true, statusCode: 200, message: 'Animal deleted' };
  }
}
