/**
 * Organization Service for SentryAgent.ai AgentIdP.
 * Business logic for multi-tenant organization lifecycle management.
 */

import { OrgRepository } from '../repositories/OrgRepository.js';
import { AgentRepository } from '../repositories/AgentRepository.js';
import {
  IOrganization,
  IOrgMember,
  ICreateOrgRequest,
  IUpdateOrgRequest,
  IAddMemberRequest,
  IPaginatedOrgsResponse,
  IOrgListFilters,
} from '../types/organization.js';
import {
  OrgNotFoundError,
  OrgHasActiveAgentsError,
  AlreadyMemberError,
  ValidationError,
  AgentNotFoundError,
} from '../utils/errors.js';

/**
 * Service for organization (tenant) lifecycle management.
 * Enforces business rules: slug uniqueness, agent limits, membership constraints.
 */
export class OrgService {
  /**
   * @param orgRepository - The organization data repository.
   * @param agentRepository - The agent repository (for membership validation).
   */
  constructor(
    private readonly orgRepository: OrgRepository,
    private readonly agentRepository: AgentRepository,
  ) {}

  /**
   * Creates a new organization.
   * Validates slug uniqueness before inserting.
   *
   * @param data - The organization creation data.
   * @returns The newly created organization record.
   * @throws ValidationError with code 'SLUG_ALREADY_EXISTS' if the slug is already taken.
   */
  async createOrg(data: ICreateOrgRequest): Promise<IOrganization> {
    const existing = await this.orgRepository.findBySlug(data.slug);
    if (existing !== null) {
      throw new ValidationError('Organization slug is already taken.', {
        code: 'SLUG_ALREADY_EXISTS',
        slug: data.slug,
      });
    }
    return this.orgRepository.create(data);
  }

  /**
   * Returns a paginated list of organizations.
   *
   * @param filters - Pagination and optional status filter.
   * @returns Paginated organizations response.
   */
  async listOrgs(filters: IOrgListFilters): Promise<IPaginatedOrgsResponse> {
    const { orgs, total } = await this.orgRepository.findAll(filters);
    return {
      data: orgs,
      total,
      page: filters.page,
      limit: filters.limit,
    };
  }

  /**
   * Retrieves a single organization by its ID.
   *
   * @param organizationId - The organization ID.
   * @returns The organization record.
   * @throws OrgNotFoundError if the organization does not exist.
   */
  async getOrg(organizationId: string): Promise<IOrganization> {
    const org = await this.orgRepository.findById(organizationId);
    if (!org) {
      throw new OrgNotFoundError(organizationId);
    }
    return org;
  }

  /**
   * Partially updates an organization's metadata.
   *
   * @param organizationId - The organization ID to update.
   * @param data - The fields to update.
   * @returns The updated organization record.
   * @throws OrgNotFoundError if the organization does not exist.
   */
  async updateOrg(organizationId: string, data: IUpdateOrgRequest): Promise<IOrganization> {
    const existing = await this.orgRepository.findById(organizationId);
    if (!existing) {
      throw new OrgNotFoundError(organizationId);
    }
    const updated = await this.orgRepository.update(organizationId, data);
    if (!updated) {
      throw new OrgNotFoundError(organizationId);
    }
    return updated;
  }

  /**
   * Soft-deletes an organization.
   * Prevents deletion if the organization has any active agents.
   *
   * @param organizationId - The organization ID to delete.
   * @throws OrgNotFoundError if the organization does not exist.
   * @throws OrgHasActiveAgentsError if there are active agents in the organization.
   */
  async deleteOrg(organizationId: string): Promise<void> {
    const existing = await this.orgRepository.findById(organizationId);
    if (!existing) {
      throw new OrgNotFoundError(organizationId);
    }

    const activeCount = await this.orgRepository.countActiveAgents(organizationId);
    if (activeCount > 0) {
      throw new OrgHasActiveAgentsError(organizationId, activeCount);
    }

    await this.orgRepository.softDelete(organizationId);
  }

  /**
   * Adds an agent as a member of an organization.
   * Validates that the organization and agent both exist, and that the agent
   * is not already a member.
   *
   * @param organizationId - The organization ID.
   * @param data - The member addition request (agentId + role).
   * @returns The created membership record.
   * @throws OrgNotFoundError if the organization does not exist.
   * @throws AgentNotFoundError if the agent does not exist.
   * @throws AlreadyMemberError if the agent is already a member of this organization.
   */
  async addMember(organizationId: string, data: IAddMemberRequest): Promise<IOrgMember> {
    const org = await this.orgRepository.findById(organizationId);
    if (!org) {
      throw new OrgNotFoundError(organizationId);
    }

    const agent = await this.agentRepository.findById(data.agentId);
    if (!agent) {
      throw new AgentNotFoundError(data.agentId);
    }

    const existingMember = await this.orgRepository.findMember(organizationId, data.agentId);
    if (existingMember !== null) {
      throw new AlreadyMemberError(data.agentId, organizationId);
    }

    return this.orgRepository.addMember(organizationId, data.agentId, data.role);
  }
}
