/**
 * GET /api/analyzer/tickets
 *
 * Browse view backing /analyzer/tickets. Filters tickets by activity window
 * + multi-axis filter set. Joins to analyzer_analyses to surface analyzed
 * state per ticket.
 *
 * Phase 2 staleness heuristic: a ticket is "stale" when
 *   tickets.last_activity_date > latest_analysis.completed_at
 * The spec calls for content-hash-based staleness; that requires either
 * caching the current hash on the tickets row or computing on read for the
 * visible page. For Phase 2 V1 we use the date heuristic — see
 * docs/ticket-analyzer-phase2-spec.md C.1 ("compute on-read for now and
 * discuss caching strategy after we see real load") and the build notes.
 *
 * Query params:
 *   period         today|yesterday|this_week|last_week|last_30d|last_60d|custom|all
 *   startDate      ISO date, only when period=custom
 *   endDate        ISO date, only when period=custom (inclusive)
 *   clientId       comma-separated companies.id values
 *   issueType      comma-separated issue_types.value values
 *   queue          comma-separated queues.value values
 *   status         comma-separated statuses.value values
 *   priority       comma-separated priorities.value values
 *   assignedTo     comma-separated resources.id values
 *   analyzed       any|yes|no|stale (default: any)
 *   needsReview    true|false (default: any)
 *   sort           created_desc|created_asc|last_activity_desc|last_activity_asc|priority
 *   search         substring match against ticket_number or title
 *   limit          default 50, max 200
 *   offset         default 0
 */

import { NextRequest, NextResponse } from 'next/server';
import { requireAuth } from '@/lib/auth-utils';
import postgresClient from '@/lib/services/postgres-client';

type Period =
  | 'today'
  | 'yesterday'
  | 'this_week'
  | 'last_week'
  | 'last_30d'
  | 'last_60d'
  | 'custom'
  | 'all';

const ALLOWED_PERIODS: ReadonlySet<Period> = new Set([
  'today',
  'yesterday',
  'this_week',
  'last_week',
  'last_30d',
  'last_60d',
  'custom',
  'all',
]);

type AnalyzedFilter = 'any' | 'yes' | 'no' | 'stale';
const ALLOWED_ANALYZED: ReadonlySet<AnalyzedFilter> = new Set([
  'any',
  'yes',
  'no',
  'stale',
]);

type SortKey =
  | 'created_desc'
  | 'created_asc'
  | 'last_activity_desc'
  | 'last_activity_asc'
  | 'priority';
const ALLOWED_SORT: ReadonlySet<SortKey> = new Set([
  'created_desc',
  'created_asc',
  'last_activity_desc',
  'last_activity_asc',
  'priority',
]);

function parseCsv(raw: string | null): number[] | null {
  if (!raw) return null;
  const parts = raw
    .split(',')
    .map((s) => Number(s.trim()))
    .filter((n) => Number.isFinite(n));
  return parts.length > 0 ? parts : null;
}

/** SQL fragment for the date predicate (no params — date math is in Postgres). */
function periodPredicate(
  period: Period,
  startDate: string | null,
  endDate: string | null
): { sql: string; params: unknown[] } {
  switch (period) {
    case 'today':
      return { sql: `t.last_activity_date >= date_trunc('day', NOW())`, params: [] };
    case 'yesterday':
      return {
        sql: `t.last_activity_date >= date_trunc('day', NOW()) - INTERVAL '1 day'
              AND t.last_activity_date < date_trunc('day', NOW())`,
        params: [],
      };
    case 'this_week':
      return { sql: `t.last_activity_date >= date_trunc('week', NOW())`, params: [] };
    case 'last_week':
      return {
        sql: `t.last_activity_date >= date_trunc('week', NOW()) - INTERVAL '1 week'
              AND t.last_activity_date < date_trunc('week', NOW())`,
        params: [],
      };
    case 'last_30d':
      return { sql: `t.last_activity_date >= NOW() - INTERVAL '30 days'`, params: [] };
    case 'last_60d':
      return { sql: `t.last_activity_date >= NOW() - INTERVAL '60 days'`, params: [] };
    case 'custom':
      // Both dates required; if missing fall through to "all".
      if (!startDate || !endDate) return { sql: `TRUE`, params: [] };
      return {
        sql: `t.last_activity_date >= $__START__ AND t.last_activity_date < ($__END__::timestamptz + INTERVAL '1 day')`,
        params: [startDate, endDate],
      };
    case 'all':
      return { sql: `TRUE`, params: [] };
  }
}

function sortClause(sort: SortKey): string {
  switch (sort) {
    case 'created_desc':
      return `f.create_date DESC NULLS LAST`;
    case 'created_asc':
      return `f.create_date ASC NULLS LAST`;
    case 'last_activity_desc':
      return `f.last_activity_date DESC NULLS LAST`;
    case 'last_activity_asc':
      return `f.last_activity_date ASC NULLS LAST`;
    case 'priority':
      // Lower priority value = higher importance in Autotask.
      return `f.priority ASC NULLS LAST, f.last_activity_date DESC NULLS LAST`;
  }
}

interface TicketRow {
  ticket_number: string;
  autotask_ticket_id: string;
  title: string | null;
  client_name: string | null;
  client_id: string | null;
  status_label: string | null;
  priority_label: string | null;
  queue_label: string | null;
  issue_type_label: string | null;
  sub_issue_type_label: string | null;
  assigned_resource_name: string | null;
  create_date: Date | null;
  last_activity_date: Date | null;
  age_in_days: number | null;
  latest_analysis_id: string | null;
  latest_analysis_at: Date | null;
  latest_completed_at: Date | null;
  needs_human_review: boolean | null;
  confidence_score: string | null;
  primary_category: string | null;
  total_count: string;
}

export async function GET(request: NextRequest) {
  const { error } = await requireAuth();
  if (error) return error;

  const url = new URL(request.url);
  const periodParam = (url.searchParams.get('period') ?? 'last_30d') as Period;
  const period: Period = ALLOWED_PERIODS.has(periodParam) ? periodParam : 'last_30d';
  const startDate = url.searchParams.get('startDate');
  const endDate = url.searchParams.get('endDate');

  const clientIds = parseCsv(url.searchParams.get('clientId'));
  const issueTypes = parseCsv(url.searchParams.get('issueType'));
  const queues = parseCsv(url.searchParams.get('queue'));
  const statuses = parseCsv(url.searchParams.get('status'));
  const priorities = parseCsv(url.searchParams.get('priority'));
  const assignedTo = parseCsv(url.searchParams.get('assignedTo'));

  const analyzedRaw = (url.searchParams.get('analyzed') ?? 'any') as AnalyzedFilter;
  const analyzed: AnalyzedFilter = ALLOWED_ANALYZED.has(analyzedRaw) ? analyzedRaw : 'any';
  const needsReviewRaw = url.searchParams.get('needsReview');
  const needsReview =
    needsReviewRaw === 'true' ? true : needsReviewRaw === 'false' ? false : null;

  const sortRaw = (url.searchParams.get('sort') ?? 'last_activity_desc') as SortKey;
  const sort: SortKey = ALLOWED_SORT.has(sortRaw) ? sortRaw : 'last_activity_desc';

  const search = (url.searchParams.get('search') ?? '').trim() || null;
  const limit = Math.min(Number(url.searchParams.get('limit') ?? 50) || 50, 200);
  const offset = Math.max(Number(url.searchParams.get('offset') ?? 0) || 0, 0);

  // Build the parameter list and SQL fragment incrementally so each filter is
  // optional. Using $N indexed params; period predicate is an SQL fragment.
  const params: unknown[] = [];
  const where: string[] = ['t.is_deleted = false'];

  const periodFragment = periodPredicate(period, startDate, endDate);
  if (periodFragment.params.length > 0) {
    params.push(...periodFragment.params);
    let frag = periodFragment.sql;
    frag = frag.replace('$__START__', `$${params.length - 1}::timestamptz`);
    frag = frag.replace('$__END__', `$${params.length}::timestamptz`);
    where.push(frag);
  } else {
    where.push(periodFragment.sql);
  }

  if (clientIds) {
    params.push(clientIds);
    where.push(`t.company_id = ANY($${params.length}::bigint[])`);
  } else {
    where.push(`t.company_id NOT IN (SELECT company_id FROM company_scope WHERE in_scope = false)`);
  }
  if (issueTypes) {
    params.push(issueTypes);
    where.push(`t.issue_type = ANY($${params.length}::int[])`);
  }
  if (queues) {
    params.push(queues);
    where.push(`t.queue_id = ANY($${params.length}::int[])`);
  }
  if (statuses) {
    params.push(statuses);
    where.push(`t.status = ANY($${params.length}::int[])`);
  }
  if (priorities) {
    params.push(priorities);
    where.push(`t.priority = ANY($${params.length}::int[])`);
  }
  if (assignedTo) {
    params.push(assignedTo);
    where.push(`t.assigned_resource_id = ANY($${params.length}::bigint[])`);
  }
  if (search) {
    params.push(search);
    where.push(
      `(t.ticket_number ILIKE '%' || $${params.length} || '%' OR t.title ILIKE '%' || $${params.length} || '%')`
    );
  }

  const analyzedHavingParts: string[] = [];
  if (analyzed === 'yes') {
    analyzedHavingParts.push(`latest.id IS NOT NULL`);
  } else if (analyzed === 'no') {
    analyzedHavingParts.push(`latest.id IS NULL`);
  } else if (analyzed === 'stale') {
    analyzedHavingParts.push(
      `latest.id IS NOT NULL AND f.last_activity_date > latest.completed_at`
    );
  }
  if (needsReview === true) {
    analyzedHavingParts.push(`latest.needs_human_review = true`);
  } else if (needsReview === false) {
    analyzedHavingParts.push(
      `(latest.needs_human_review IS DISTINCT FROM true)`
    );
  }
  const havingFragment =
    analyzedHavingParts.length > 0
      ? `WHERE ${analyzedHavingParts.join(' AND ')}`
      : '';

  params.push(limit);
  const limitParamIdx = params.length;
  params.push(offset);
  const offsetParamIdx = params.length;

  const sql = `
    WITH filtered AS (
      SELECT t.id, t.ticket_number, t.title,
             t.company_id, t.issue_type, t.sub_issue_type,
             t.status, t.priority, t.queue_id,
             t.assigned_resource_id,
             t.create_date, t.last_activity_date
        FROM tickets t
       WHERE ${where.join(' AND ')}
    )
    SELECT f.ticket_number,
           f.id::text AS autotask_ticket_id,
           f.title,
           c.company_name AS client_name,
           c.id::text     AS client_id,
           s.label    AS status_label,
           pr.label   AS priority_label,
           q.label    AS queue_label,
           it.label   AS issue_type_label,
           sit.label  AS sub_issue_type_label,
           CASE WHEN r.id IS NOT NULL
                THEN trim(coalesce(r.first_name,'') || ' ' || coalesce(r.last_name,''))
                ELSE NULL
           END AS assigned_resource_name,
           f.create_date,
           f.last_activity_date,
           CASE WHEN f.create_date IS NULL THEN NULL
                ELSE EXTRACT(DAY FROM NOW() - f.create_date)::int
           END AS age_in_days,
           latest.id::text             AS latest_analysis_id,
           latest.triggered_at         AS latest_analysis_at,
           latest.completed_at         AS latest_completed_at,
           latest.needs_human_review,
           latest.confidence_score::text AS confidence_score,
           (latest.aggregate_fingerprint ->> 'category') AS primary_category,
           COUNT(*) OVER () AS total_count
      FROM filtered f
      LEFT JOIN companies   c   ON c.id    = f.company_id
      LEFT JOIN issue_types it  ON it.value = f.issue_type
      LEFT JOIN issue_types sit ON sit.value = f.sub_issue_type
      LEFT JOIN statuses    s   ON s.value  = f.status
      LEFT JOIN priorities  pr  ON pr.value = f.priority
      LEFT JOIN queues      q   ON q.value  = f.queue_id
      LEFT JOIN resources   r   ON r.id     = f.assigned_resource_id
      LEFT JOIN LATERAL (
        SELECT aa.id, aa.triggered_at, aa.completed_at,
               aa.needs_human_review, aa.confidence_score,
               aa.aggregate_fingerprint, aa.analysis_version
          FROM analyzer_analyses aa
         WHERE aa.ticket_number = f.ticket_number
           AND aa.status = 'complete'
         ORDER BY aa.analysis_version DESC
         LIMIT 1
      ) latest ON TRUE
      ${havingFragment}
     ORDER BY ${sortClause(sort)}
     LIMIT $${limitParamIdx} OFFSET $${offsetParamIdx}
  `;

  const res = await postgresClient.query<TicketRow>(sql, params);
  const total = res.rows.length > 0 ? Number(res.rows[0].total_count) : 0;

  const tickets = res.rows.map((r) => {
    let analyzedState: 'none' | 'current' | 'stale' = 'none';
    if (r.latest_analysis_id) {
      if (
        r.last_activity_date &&
        r.latest_completed_at &&
        r.last_activity_date.getTime() > r.latest_completed_at.getTime()
      ) {
        analyzedState = 'stale';
      } else {
        analyzedState = 'current';
      }
    }
    return {
      ticketNumber: r.ticket_number,
      autotaskTicketId: Number(r.autotask_ticket_id),
      title: r.title,
      clientName: r.client_name,
      clientId: r.client_id ? Number(r.client_id) : null,
      status: r.status_label,
      priority: r.priority_label,
      queue: r.queue_label,
      issueType: r.issue_type_label,
      subIssueType: r.sub_issue_type_label,
      assignedResourceName: r.assigned_resource_name,
      createdAtAutotask: r.create_date ? r.create_date.toISOString() : null,
      lastActivityAtAutotask: r.last_activity_date
        ? r.last_activity_date.toISOString()
        : null,
      ageInDays: r.age_in_days,
      analyzedState,
      latestAnalysisId: r.latest_analysis_id,
      latestAnalysisAt: r.latest_analysis_at
        ? r.latest_analysis_at.toISOString()
        : null,
      needsHumanReview: r.needs_human_review ?? false,
      confidenceScore: r.confidence_score === null ? null : Number(r.confidence_score),
      primaryCategory: r.primary_category,
    };
  });

  return NextResponse.json({
    tickets,
    total,
    filters: {
      period,
      startDate,
      endDate,
      clientIds,
      issueTypes,
      queues,
      statuses,
      priorities,
      assignedTo,
      analyzed,
      needsReview,
      sort,
      search,
      limit,
      offset,
    },
  });
}
