import { Prisma, CutCategory } from '@prisma/client';
import { prisma } from '../../../config/database';
import { AppError } from '../../../middleware/error-handler';
import { isPointInPolygon, parseGeoJsonPolygon, calculateBounds } from '../../../utils/spatial';
import { logger } from '../../../utils/logger';
import type { CreateCutInput, UpdateCutInput, ListCutsInput } from './cuts.schemas';

export const cutsService = {
  async findAll(filters: ListCutsInput) {
    const { page, limit, category, search } = filters;
    const skip = (page - 1) * limit;

    const where: Prisma.CutWhereInput = {};

    if (search) {
      where.OR = [
        { name: { contains: search, mode: 'insensitive' } },
        { description: { contains: search, mode: 'insensitive' } },
      ];
    }

    if (category) where.category = category;

    const [cuts, total] = await Promise.all([
      prisma.cut.findMany({
        where,
        skip,
        take: limit,
        orderBy: { createdAt: 'desc' },
      }),
      prisma.cut.count({ where }),
    ]);

    return {
      cuts,
      pagination: {
        page,
        limit,
        total,
        totalPages: Math.ceil(total / limit),
      },
    };
  },

  async findById(id: string) {
    const cut = await prisma.cut.findUnique({ where: { id } });
    if (!cut) {
      throw new AppError(404, 'Cut not found', 'CUT_NOT_FOUND');
    }
    return cut;
  },

  async create(data: CreateCutInput, userId: string) {
    // Auto-calculate bounds from geojson if not provided
    let boundsStr = data.bounds;
    if (!boundsStr) {
      try {
        const rings = parseGeoJsonPolygon(data.geojson);
        const allCoords = rings.flat();
        const bounds = calculateBounds(allCoords);
        boundsStr = JSON.stringify(bounds);
      } catch {
        // Bounds calculation optional
      }
    }

    const cut = await prisma.cut.create({
      data: {
        name: data.name,
        description: data.description,
        color: data.color,
        opacity: data.opacity,
        category: data.category,
        isPublic: data.isPublic,
        isOfficial: data.isOfficial,
        geojson: data.geojson,
        bounds: boundsStr,
        showLocations: data.showLocations,
        exportEnabled: data.exportEnabled,
        assignedTo: data.assignedTo,
        createdByUserId: userId,
      },
    });

    return cut;
  },

  async update(id: string, data: UpdateCutInput) {
    const existing = await prisma.cut.findUnique({ where: { id } });
    if (!existing) {
      throw new AppError(404, 'Cut not found', 'CUT_NOT_FOUND');
    }

    // Recalculate bounds if geojson changed
    const updateData: Prisma.CutUncheckedUpdateInput = { ...data };

    if (data.geojson && !data.bounds) {
      try {
        const rings = parseGeoJsonPolygon(data.geojson);
        const allCoords = rings.flat();
        const bounds = calculateBounds(allCoords);
        updateData.bounds = JSON.stringify(bounds);
      } catch {
        // Bounds calculation optional
      }
    }

    if (data.lastCanvassed !== undefined) {
      updateData.lastCanvassed = data.lastCanvassed ? new Date(data.lastCanvassed) : null;
    }

    const cut = await prisma.cut.update({
      where: { id },
      data: updateData,
    });

    return cut;
  },

  async delete(id: string) {
    const existing = await prisma.cut.findUnique({ where: { id } });
    if (!existing) {
      throw new AppError(404, 'Cut not found', 'CUT_NOT_FOUND');
    }
    await prisma.cut.delete({ where: { id } });
  },

  async getPublicCuts(limit: number = 50) {
    const cuts = await prisma.cut.findMany({
      where: { isPublic: true },
      select: {
        id: true,
        name: true,
        description: true,
        color: true,
        opacity: true,
        category: true,
        geojson: true,
        bounds: true,
      },
      orderBy: { name: 'asc' },
      take: limit,
    });
    return cuts;
  },

  async getLocationsInCut(id: string) {
    const cut = await prisma.cut.findUnique({ where: { id } });
    if (!cut) {
      throw new AppError(404, 'Cut not found', 'CUT_NOT_FOUND');
    }

    const rings = parseGeoJsonPolygon(cut.geojson);

    // Get all locations (latitude/longitude are non-nullable)
    const locations = await prisma.location.findMany();

    // Filter to those inside the polygon
    const inside = locations.filter((loc) => {
      const lat = Number(loc.latitude);
      const lng = Number(loc.longitude);
      return rings.some((ring) => isPointInPolygon(lat, lng, ring));
    });

    return inside;
  },

  async exportSingleGeoJson(id: string) {
    const cut = await prisma.cut.findUnique({ where: { id } });
    if (!cut) throw new AppError(404, 'Cut not found', 'CUT_NOT_FOUND');

    const geojson = JSON.parse(cut.geojson);
    return {
      type: 'Feature' as const,
      properties: {
        name: cut.name,
        description: cut.description,
        color: cut.color,
        'fill-opacity': Number(cut.opacity),
        fill: cut.color,
        category: cut.category,
        isPublic: cut.isPublic,
        isOfficial: cut.isOfficial,
        assignedTo: cut.assignedTo,
      },
      geometry: geojson,
    };
  },

  async exportAllGeoJson() {
    const cuts = await prisma.cut.findMany({ orderBy: { name: 'asc' } });

    const features = cuts.map((cut) => {
      const geojson = JSON.parse(cut.geojson);
      return {
        type: 'Feature' as const,
        properties: {
          name: cut.name,
          description: cut.description,
          color: cut.color,
          'fill-opacity': Number(cut.opacity),
          fill: cut.color,
          category: cut.category,
          isPublic: cut.isPublic,
          isOfficial: cut.isOfficial,
          assignedTo: cut.assignedTo,
        },
        geometry: geojson,
      };
    });

    return {
      type: 'FeatureCollection' as const,
      features,
    };
  },

  async importGeoJson(buffer: Buffer, userId: string) {
    let parsed: Record<string, unknown>;
    try {
      parsed = JSON.parse(buffer.toString('utf-8'));
    } catch {
      throw new AppError(400, 'Invalid JSON file', 'INVALID_JSON');
    }

    // Collect features from various GeoJSON shapes
    const features: Array<{ geometry: Record<string, unknown>; properties?: Record<string, unknown> }> = [];
    const type = parsed.type as string;

    if (type === 'FeatureCollection' && Array.isArray(parsed.features)) {
      for (const f of parsed.features as Array<Record<string, unknown>>) {
        if (f.geometry) features.push({ geometry: f.geometry as Record<string, unknown>, properties: (f.properties ?? {}) as Record<string, unknown> });
      }
    } else if (type === 'Feature' && parsed.geometry) {
      features.push({ geometry: parsed.geometry as Record<string, unknown>, properties: (parsed.properties ?? {}) as Record<string, unknown> });
    } else if (type === 'Polygon' || type === 'MultiPolygon') {
      features.push({ geometry: parsed, properties: {} });
    } else {
      throw new AppError(400, 'Unsupported GeoJSON type. Expected Feature, FeatureCollection, Polygon, or MultiPolygon.', 'UNSUPPORTED_GEOJSON');
    }

    let success = 0;
    let failed = 0;
    const errors: string[] = [];

    for (let i = 0; i < features.length; i++) {
      try {
        const { geometry, properties } = features[i]!;
        const geoType = geometry.type as string;

        if (geoType !== 'Polygon' && geoType !== 'MultiPolygon') {
          errors.push(`Feature ${i + 1}: Unsupported geometry type "${geoType}"`);
          failed++;
          continue;
        }

        const geojsonStr = JSON.stringify(geometry);

        // Extract properties
        const name = (properties?.name as string) || `Imported Cut ${i + 1}`;
        const color = (properties?.color as string) || (properties?.fill as string) || '#3388ff';
        const opacity = typeof properties?.['fill-opacity'] === 'number' ? properties['fill-opacity'] : 0.3;
        const categoryRaw = (properties?.category as string)?.toUpperCase();
        const category = categoryRaw && Object.values(CutCategory).includes(categoryRaw as CutCategory)
          ? (categoryRaw as CutCategory)
          : undefined;

        // Calculate bounds
        let boundsStr: string | undefined;
        try {
          const rings = parseGeoJsonPolygon(geojsonStr);
          const allCoords = rings.flat();
          const bounds = calculateBounds(allCoords);
          boundsStr = JSON.stringify(bounds);
        } catch { /* optional */ }

        await prisma.cut.create({
          data: {
            name,
            description: (properties?.description as string) || undefined,
            color: /^#[0-9a-fA-F]{6}$/.test(color) ? color : '#3388ff',
            opacity,
            category,
            isPublic: properties?.isPublic === true,
            isOfficial: properties?.isOfficial === true,
            geojson: geojsonStr,
            bounds: boundsStr,
            assignedTo: (properties?.assignedTo as string) || undefined,
            createdByUserId: userId,
          },
        });
        success++;
      } catch (err) {
        const msg = err instanceof Error ? err.message : 'Unknown error';
        errors.push(`Feature ${i + 1}: ${msg}`);
        failed++;
        logger.warn(`GeoJSON import feature ${i + 1} failed:`, err);
      }
    }

    return { total: features.length, success, failed, errors: errors.slice(0, 50) };
  },

  async getStatistics(id: string) {
    const cut = await prisma.cut.findUnique({ where: { id } });
    if (!cut) {
      throw new AppError(404, 'Cut not found', 'CUT_NOT_FOUND');
    }

    const rings = parseGeoJsonPolygon(cut.geojson);

    // Get all locations with addresses (latitude/longitude are non-nullable)
    const locations = await prisma.location.findMany({
      select: {
        id: true,
        latitude: true,
        longitude: true,
        addresses: {
          select: {
            id: true,
            supportLevel: true,
            sign: true,
          },
        },
      },
    });

    // Filter locations inside polygon and flatten addresses
    const addressesInCut = [];
    for (const loc of locations) {
      const lat = Number(loc.latitude);
      const lng = Number(loc.longitude);
      if (rings.some((ring) => isPointInPolygon(lat, lng, ring))) {
        addressesInCut.push(...loc.addresses);
      }
    }

    const byLevel: Record<string, number> = {
      LEVEL_1: 0,
      LEVEL_2: 0,
      LEVEL_3: 0,
      LEVEL_4: 0,
      NONE: 0,
    };

    let withSign = 0;

    for (const addr of addressesInCut) {
      const level = addr.supportLevel || 'NONE';
      byLevel[level] = (byLevel[level] || 0) + 1;
      if (addr.sign) withSign++;
    }

    return {
      total: addressesInCut.length,
      byLevel,
      withSign,
    };
  },
};
