/**
 * Federation get query layer (FED-M3-06).
 *
 * Read-only DB adapter used by GetController after FederationAuthGuard and
 * FederationScopeService have established the subject user, allowed resource,
 * native-RBAC intersection, and row cap. Audit writes are intentionally
 * deferred to M4.
 */

import { Inject, Injectable } from '@nestjs/common';
import {
  and,
  eq,
  inArray,
  insights,
  or,
  missionTasks,
  missions,
  preferences,
  projects,
  tasks,
  teamMembers,
  type Db,
} from '@mosaicstack/db';
import { DB } from '../../../database/database.module.js';
import type {
  FederationNativeRbacEvaluator,
  FederationNativeRbacRequest,
  FederationNativeRbacResult,
  FederationScopeQueryFilter,
} from '../scope.service.js';

export interface FederationGetQueryRequest {
  readonly filter: FederationScopeQueryFilter;
  readonly id: string;
}

export interface FederationGetQueryFoundResult<T extends object = Record<string, unknown>> {
  readonly status: 'found';
  readonly item: T;
}

export interface FederationGetQueryNotFoundResult {
  readonly status: 'not_found';
}

export interface FederationGetQueryDeniedResult {
  readonly status: 'denied';
  readonly reason: string;
}

export type FederationGetQueryResult<T extends object = Record<string, unknown>> =
  | FederationGetQueryFoundResult<T>
  | FederationGetQueryNotFoundResult
  | FederationGetQueryDeniedResult;

type RowObject = Record<string, unknown>;

function firstRow<T>(rows: T[]): T | undefined {
  return rows[0];
}

function rowBelongsToAccessibleProjectOrMission(
  row: { projectId?: string | null; missionId?: string | null },
  projectIds: readonly string[],
  missionIds: readonly string[],
): boolean {
  return (
    (typeof row.projectId === 'string' && projectIds.includes(row.projectId)) ||
    (typeof row.missionId === 'string' && missionIds.includes(row.missionId))
  );
}

@Injectable()
export class FederationGetQueryService implements FederationNativeRbacEvaluator {
  constructor(@Inject(DB) private readonly db: Db) {}

  async evaluateReadAccess(
    request: FederationNativeRbacRequest,
  ): Promise<FederationNativeRbacResult> {
    if (request.resource === 'credentials' || request.resource === 'api_keys') {
      return {
        allowed: false,
        reason: `${request.resource} federation get access is not implemented in M3`,
        details: { resource: request.resource },
      };
    }

    if (request.resource === 'memory') {
      return { allowed: true, access: { includePersonal: true, teamIds: [] } };
    }

    const teamIds = await this.listSubjectTeamIds(request.subjectUserId);
    return { allowed: true, access: { includePersonal: true, teamIds } };
  }

  async get<T extends RowObject = RowObject>(
    request: FederationGetQueryRequest,
  ): Promise<FederationGetQueryResult<T>> {
    return this.getByResource(request.filter, request.id) as Promise<FederationGetQueryResult<T>>;
  }

  private async getByResource(
    filter: FederationScopeQueryFilter,
    id: string,
  ): Promise<FederationGetQueryResult> {
    switch (filter.resource) {
      case 'tasks':
        return this.getTask(filter, id);
      case 'notes':
        return this.getNote(filter, id);
      case 'memory':
        return this.getMemory(filter, id);
      case 'credentials':
      case 'api_keys':
        return { status: 'denied', reason: `${filter.resource} federation get is not implemented` };
      default:
        return {
          status: 'denied',
          reason: `Unsupported federation get resource: ${String(filter.resource)}`,
        };
    }
  }

  private async listSubjectTeamIds(subjectUserId: string): Promise<string[]> {
    const rows = await this.db
      .select({ teamId: teamMembers.teamId })
      .from(teamMembers)
      .where(eq(teamMembers.userId, subjectUserId));

    return rows.map((row) => row.teamId);
  }

  private async listAccessibleProjectIds(filter: FederationScopeQueryFilter): Promise<string[]> {
    const clauses = [];
    if (filter.includePersonal) {
      clauses.push(and(eq(projects.ownerType, 'user'), eq(projects.ownerId, filter.subjectUserId)));
    }
    if (filter.teamIds.length > 0) {
      // Project team ownership follows TeamsService.canAccessProject: team-owned
      // rows are authorized through projects.teamId, while ownerId remains the
      // user who created/bootstrapped the project.
      clauses.push(
        and(eq(projects.ownerType, 'team'), inArray(projects.teamId, [...filter.teamIds])),
      );
    }

    if (clauses.length === 0) {
      return [];
    }

    const rows = await this.db
      .select({ id: projects.id })
      .from(projects)
      .where(clauses.length === 1 ? clauses[0] : or(...clauses));

    return rows.map((row) => row.id);
  }

  private async listMissionIds(projectIds: readonly string[]): Promise<string[]> {
    if (projectIds.length === 0) {
      return [];
    }

    const rows = await this.db
      .select({ id: missions.id })
      .from(missions)
      .where(inArray(missions.projectId, [...projectIds]));

    return rows.map((row) => row.id);
  }

  private async getTask(
    filter: FederationScopeQueryFilter,
    id: string,
  ): Promise<FederationGetQueryResult> {
    const row = firstRow(
      await this.db
        .select({
          id: tasks.id,
          title: tasks.title,
          description: tasks.description,
          status: tasks.status,
          priority: tasks.priority,
          projectId: tasks.projectId,
          missionId: tasks.missionId,
          assignee: tasks.assignee,
          tags: tasks.tags,
          dueDate: tasks.dueDate,
          metadata: tasks.metadata,
          createdAt: tasks.createdAt,
          updatedAt: tasks.updatedAt,
        })
        .from(tasks)
        .where(eq(tasks.id, id))
        .limit(1),
    );

    if (!row) {
      return { status: 'not_found' };
    }

    const projectIds = await this.listAccessibleProjectIds(filter);
    const missionIds = await this.listMissionIds(projectIds);
    if (!rowBelongsToAccessibleProjectOrMission(row, projectIds, missionIds)) {
      return { status: 'denied', reason: 'Task is outside the federated scope' };
    }

    return { status: 'found', item: row as RowObject };
  }

  private async getNote(
    filter: FederationScopeQueryFilter,
    id: string,
  ): Promise<FederationGetQueryResult> {
    const row = firstRow(
      await this.db
        .select({
          id: missionTasks.id,
          missionId: missionTasks.missionId,
          taskId: missionTasks.taskId,
          userId: missionTasks.userId,
          status: missionTasks.status,
          content: missionTasks.notes,
          createdAt: missionTasks.createdAt,
          updatedAt: missionTasks.updatedAt,
        })
        .from(missionTasks)
        .where(eq(missionTasks.id, id))
        .limit(1),
    );

    if (!row || row.content === null || row.content === '') {
      return { status: 'not_found' };
    }

    const projectIds = await this.listAccessibleProjectIds(filter);
    const missionIds = await this.listMissionIds(projectIds);

    // mission_tasks rows are user-scoped even when the mission belongs to a team.
    // Scope-visible missions must intersect with subject ownership; team scope
    // narrows mission IDs but never widens note reads to another user's rows.
    if (row.userId !== filter.subjectUserId || !missionIds.includes(row.missionId)) {
      return { status: 'denied', reason: 'Note is outside the federated scope' };
    }

    const item = { ...row } as RowObject;
    delete item['userId'];
    return { status: 'found', item };
  }

  private async getMemory(
    filter: FederationScopeQueryFilter,
    id: string,
  ): Promise<FederationGetQueryResult> {
    const [insightRow, preferenceRow] = await Promise.all([
      this.db
        .select({
          id: insights.id,
          userId: insights.userId,
          kind: insights.source,
          content: insights.content,
          category: insights.category,
          relevanceScore: insights.relevanceScore,
          metadata: insights.metadata,
          createdAt: insights.createdAt,
          updatedAt: insights.updatedAt,
        })
        .from(insights)
        .where(eq(insights.id, id))
        .limit(1)
        .then(firstRow),
      this.db
        .select({
          id: preferences.id,
          userId: preferences.userId,
          kind: preferences.category,
          key: preferences.key,
          value: preferences.value,
          source: preferences.source,
          mutable: preferences.mutable,
          createdAt: preferences.createdAt,
          updatedAt: preferences.updatedAt,
        })
        .from(preferences)
        .where(eq(preferences.id, id))
        .limit(1)
        .then(firstRow),
    ]);

    const candidates = [insightRow, preferenceRow].filter(
      (row): row is NonNullable<typeof row> => row !== undefined,
    );
    if (candidates.length === 0) {
      return { status: 'not_found' };
    }

    if (!filter.includePersonal) {
      return { status: 'denied', reason: 'Memory personal rows are outside the federated scope' };
    }

    const accessible = candidates.find((row) => row.userId === filter.subjectUserId);
    if (!accessible) {
      return { status: 'denied', reason: 'Memory row belongs to another subject user' };
    }

    const item = { ...accessible } as RowObject;
    delete item['userId'];
    return { status: 'found', item };
  }
}
