const crypto = require("crypto");
const net = require("net");
const invitesModel = require("../models/group-invites.model");
const { inviteCodeLast4 } = require("../utils/redaction");

const JOIN_POLICIES = Object.freeze({
  NOT_ACCEPTING: "NOT_ACCEPTING",
  AUTO_ACCEPT: "AUTO_ACCEPT",
  APPROVAL_REQUIRED: "APPROVAL_REQUIRED",
});

const JOIN_RESULTS = Object.freeze({
  JOINED: "JOINED",
  PENDING: "PENDING",
  ALREADY_MEMBER: "ALREADY_MEMBER",
});

class InviteServiceError extends Error {
  constructor(code, message, statusCode = 400) {
    super(message);
    this.name = "InviteServiceError";
    this.code = code;
    this.statusCode = statusCode;
  }
}

function normalizeIp(ip) {
  if (!ip || typeof ip !== "string") return null;
  const trimmed = ip.trim();
  if (!trimmed) return null;
  return net.isIP(trimmed) ? trimmed : null;
}

function ensureJoinPolicy(policy) {
  if (Object.values(JOIN_POLICIES).includes(policy)) {
    return policy;
  }
  throw new InviteServiceError(
    "INVALID_JOIN_POLICY",
    "Invalid join policy",
    400
  );
}

function ensurePositiveInteger(value, fieldName) {
  const parsed = Number.parseInt(value, 10);
  if (!Number.isInteger(parsed) || parsed <= 0) {
    throw new InviteServiceError("INVALID_INPUT", `${fieldName} is required`, 400);
  }
  return parsed;
}

function ensureDate(value, fieldName) {
  const date = value instanceof Date ? value : new Date(value);
  if (Number.isNaN(date.getTime())) {
    throw new InviteServiceError("INVALID_INPUT", `${fieldName} is invalid`, 400);
  }
  return date;
}

async function ensureGroupAndManagerRole(userId, groupId, client) {
  const group = await invitesModel.getGroupById(groupId, client);
  if (!group) {
    throw new InviteServiceError("GROUP_NOT_FOUND", "Group not found", 404);
  }

  const actorRole = await invitesModel.getUserGroupRole(groupId, userId, client);
  if (!["owner", "admin"].includes(actorRole)) {
    throw new InviteServiceError(
      "FORBIDDEN",
      "Admin or owner role required",
      403
    );
  }

  return { actorRole, group };
}

async function resolveManagedGroupId(userId, requestedGroupId) {
  if (requestedGroupId !== undefined && requestedGroupId !== null) {
    return ensurePositiveInteger(requestedGroupId, "groupId");
  }

  const manageableGroups = await invitesModel.getManageableGroupsForUser(userId);
  if (manageableGroups.length === 0) {
    throw new InviteServiceError(
      "FORBIDDEN",
      "Admin or owner role required",
      403
    );
  }

  if (manageableGroups.length > 1) {
    throw new InviteServiceError(
      "GROUP_ID_REQUIRED",
      "Group ID is required when you manage multiple groups",
      400
    );
  }

  return manageableGroups[0].group_id;
}

async function createInviteLink(
  userId,
  groupId,
  policy,
  singleUse,
  expiresAt,
  requestId,
  ip,
  userAgent
) {
  const resolvedGroupId = ensurePositiveInteger(groupId, "groupId");
  const resolvedPolicy = ensureJoinPolicy(policy);
  const resolvedExpiresAt = ensureDate(expiresAt, "expiresAt");

  return invitesModel.withTransaction(async (client) => {
    const { actorRole } = await ensureGroupAndManagerRole(
      userId,
      resolvedGroupId,
      client
    );

    let link = null;
    for (let attempt = 0; attempt < 5; attempt += 1) {
      const token = crypto.randomBytes(16).toString("hex");
      try {
        link = await invitesModel.createInviteLink(
          {
            groupId: resolvedGroupId,
            createdBy: userId,
            token,
            policy: resolvedPolicy,
            singleUse: Boolean(singleUse),
            expiresAt: resolvedExpiresAt,
          },
          client
        );
        break;
      } catch (error) {
        if (error.code !== "23505") {
          throw error;
        }
      }
    }

    if (!link) {
      throw new InviteServiceError(
        "INVITE_CREATE_FAILED",
        "Unable to create invite link",
        500
      );
    }

    await invitesModel.createGroupAuditLog(
      {
        groupId: resolvedGroupId,
        actorUserId: userId,
        actorRole,
        eventType: "GROUP_INVITE_CREATED",
        requestId,
        ip: normalizeIp(ip),
        userAgent: userAgent || null,
        metadata: {
          inviteCodeLast4: inviteCodeLast4(link.token),
        },
      },
      client
    );

    return link;
  });
}

async function listInviteLinks(userId, groupId) {
  const resolvedGroupId = ensurePositiveInteger(groupId, "groupId");
  await ensureGroupAndManagerRole(userId, resolvedGroupId);
  return invitesModel.listInviteLinks(resolvedGroupId);
}

async function revokeInviteLink(
  userId,
  groupId,
  linkId,
  requestId,
  ip,
  userAgent
) {
  const resolvedGroupId = ensurePositiveInteger(groupId, "groupId");
  const resolvedLinkId = ensurePositiveInteger(linkId, "linkId");

  return invitesModel.withTransaction(async (client) => {
    const { actorRole } = await ensureGroupAndManagerRole(
      userId,
      resolvedGroupId,
      client
    );
    const link = await invitesModel.revokeInviteLink(
      resolvedGroupId,
      resolvedLinkId,
      client
    );
    if (!link) {
      throw new InviteServiceError("INVITE_NOT_FOUND", "Invite link not found", 404);
    }

    await invitesModel.createGroupAuditLog(
      {
        groupId: resolvedGroupId,
        actorUserId: userId,
        actorRole,
        eventType: "GROUP_INVITE_REVOKED",
        requestId,
        ip: normalizeIp(ip),
        userAgent: userAgent || null,
        metadata: {
          inviteCodeLast4: inviteCodeLast4(link.token),
        },
      },
      client
    );
  });
}

async function reviveInviteLink(
  userId,
  groupId,
  linkId,
  expiresAt,
  requestId,
  ip,
  userAgent
) {
  const resolvedGroupId = ensurePositiveInteger(groupId, "groupId");
  const resolvedLinkId = ensurePositiveInteger(linkId, "linkId");
  const resolvedExpiresAt = ensureDate(expiresAt, "expiresAt");

  return invitesModel.withTransaction(async (client) => {
    const { actorRole } = await ensureGroupAndManagerRole(
      userId,
      resolvedGroupId,
      client
    );
    const link = await invitesModel.reviveInviteLink(
      resolvedGroupId,
      resolvedLinkId,
      resolvedExpiresAt,
      client
    );
    if (!link) {
      throw new InviteServiceError("INVITE_NOT_FOUND", "Invite link not found", 404);
    }

    await invitesModel.createGroupAuditLog(
      {
        groupId: resolvedGroupId,
        actorUserId: userId,
        actorRole,
        eventType: "GROUP_INVITE_REVIVED",
        requestId,
        ip: normalizeIp(ip),
        userAgent: userAgent || null,
        metadata: {
          inviteCodeLast4: inviteCodeLast4(link.token),
        },
      },
      client
    );
  });
}

async function deleteInviteLink(
  userId,
  groupId,
  linkId,
  requestId,
  ip,
  userAgent
) {
  const resolvedGroupId = ensurePositiveInteger(groupId, "groupId");
  const resolvedLinkId = ensurePositiveInteger(linkId, "linkId");

  return invitesModel.withTransaction(async (client) => {
    const { actorRole } = await ensureGroupAndManagerRole(
      userId,
      resolvedGroupId,
      client
    );
    const link = await invitesModel.deleteInviteLink(
      resolvedGroupId,
      resolvedLinkId,
      client
    );
    if (!link) {
      throw new InviteServiceError("INVITE_NOT_FOUND", "Invite link not found", 404);
    }

    await invitesModel.createGroupAuditLog(
      {
        groupId: resolvedGroupId,
        actorUserId: userId,
        actorRole,
        eventType: "GROUP_INVITE_DELETED",
        requestId,
        ip: normalizeIp(ip),
        userAgent: userAgent || null,
        metadata: {
          inviteCodeLast4: inviteCodeLast4(link.token),
        },
      },
      client
    );
  });
}

function getInviteStatus(link) {
  const now = Date.now();
  if (link.single_use && link.used_at) return "USED";
  if (link.revoked_at) return "REVOKED";
  if (new Date(link.expires_at).getTime() <= now) return "EXPIRED";
  return "ACTIVE";
}

async function getInviteLinkSummaryByToken(token, userId = null) {
  if (!token || typeof token !== "string") {
    throw new InviteServiceError("INVALID_INVITE_TOKEN", "Invite token is required", 400);
  }

  const summary = await invitesModel.getInviteLinkSummaryByToken(token.trim());
  if (!summary) {
    throw new InviteServiceError("INVITE_NOT_FOUND", "Invite link not found", 404);
  }

  let viewerStatus = null;
  if (userId) {
    const isMember = await invitesModel.isGroupMember(summary.group_id, userId);
    if (isMember) {
      viewerStatus = JOIN_RESULTS.ALREADY_MEMBER;
    } else {
      const pending = await invitesModel.getPendingJoinRequest(summary.group_id, userId);
      if (pending) {
        viewerStatus = JOIN_RESULTS.PENDING;
      }
    }
  }

  const activePolicy = summary.current_join_policy || summary.policy;
  return {
    id: summary.id,
    group_id: summary.group_id,
    group_name: summary.group_name,
    token: summary.token,
    policy: summary.policy,
    current_join_policy: summary.current_join_policy || null,
    active_policy: activePolicy,
    single_use: summary.single_use,
    expires_at: summary.expires_at,
    used_at: summary.used_at,
    revoked_at: summary.revoked_at,
    created_at: summary.created_at,
    status: getInviteStatus(summary),
    ...(viewerStatus ? { viewerStatus } : {}),
  };
}

async function acceptInviteLink(userId, token, requestId, ip, userAgent) {
  if (!userId) {
    throw new InviteServiceError("UNAUTHORIZED", "Authentication required", 401);
  }
  if (!token || typeof token !== "string") {
    throw new InviteServiceError("INVALID_INVITE_TOKEN", "Invite token is required", 400);
  }

  return invitesModel.withTransaction(async (client) => {
    const summary = await invitesModel.getInviteLinkSummaryByToken(
      token.trim(),
      client,
      true
    );
    if (!summary) {
      throw new InviteServiceError("INVITE_NOT_FOUND", "Invite link not found", 404);
    }

    const group = {
      id: summary.group_id,
      name: summary.group_name,
    };

    const memberExists = await invitesModel.isGroupMember(summary.group_id, userId, client);
    if (memberExists) {
      return { status: JOIN_RESULTS.ALREADY_MEMBER, group };
    }

    const pending = await invitesModel.getPendingJoinRequest(summary.group_id, userId, client);
    if (pending) {
      return { status: JOIN_RESULTS.PENDING, group };
    }

    const now = Date.now();
    if (summary.revoked_at) {
      throw new InviteServiceError(
        "INVITE_REVOKED",
        "This invite link has been revoked",
        410
      );
    }
    if (new Date(summary.expires_at).getTime() <= now) {
      throw new InviteServiceError(
        "INVITE_EXPIRED",
        "This invite link has expired",
        410
      );
    }
    if (summary.single_use && summary.used_at) {
      throw new InviteServiceError(
        "INVITE_USED",
        "This invite link has already been used",
        410
      );
    }

    const activePolicy =
      summary.current_join_policy || summary.policy || JOIN_POLICIES.NOT_ACCEPTING;
    if (activePolicy === JOIN_POLICIES.NOT_ACCEPTING) {
      throw new InviteServiceError(
        "JOIN_NOT_ACCEPTING",
        "This group is not accepting new members",
        403
      );
    }

    const actorRole = (await invitesModel.getUserGroupRole(summary.group_id, userId, client)) || "guest";

    if (activePolicy === JOIN_POLICIES.AUTO_ACCEPT) {
      const inserted = await invitesModel.addGroupMember(
        summary.group_id,
        userId,
        "member",
        client
      );
      if (!inserted) {
        return { status: JOIN_RESULTS.ALREADY_MEMBER, group };
      }

      if (summary.single_use) {
        await invitesModel.consumeSingleUseInvite(summary.id, client);
      }

      await invitesModel.createGroupAuditLog(
        {
          groupId: summary.group_id,
          actorUserId: userId,
          actorRole,
          eventType: "GROUP_INVITE_USED",
          requestId,
          ip: normalizeIp(ip),
          userAgent: userAgent || null,
          metadata: {
            inviteCodeLast4: inviteCodeLast4(summary.token),
          },
        },
        client
      );

      return { status: JOIN_RESULTS.JOINED, group };
    }

    if (activePolicy === JOIN_POLICIES.APPROVAL_REQUIRED) {
      await invitesModel.createOrTouchPendingJoinRequest(summary.group_id, userId, client);

      if (summary.single_use) {
        await invitesModel.consumeSingleUseInvite(summary.id, client);
      }

      await invitesModel.createGroupAuditLog(
        {
          groupId: summary.group_id,
          actorUserId: userId,
          actorRole,
          eventType: "GROUP_INVITE_REQUESTED",
          requestId,
          ip: normalizeIp(ip),
          userAgent: userAgent || null,
          metadata: {
            inviteCodeLast4: inviteCodeLast4(summary.token),
          },
        },
        client
      );

      return { status: JOIN_RESULTS.PENDING, group };
    }

    throw new InviteServiceError("INVALID_JOIN_POLICY", "Invalid join policy", 400);
  });
}

async function getGroupJoinPolicy(userId, groupId) {
  const resolvedGroupId = ensurePositiveInteger(groupId, "groupId");
  await ensureGroupAndManagerRole(userId, resolvedGroupId);
  const settings = await invitesModel.getGroupSettings(resolvedGroupId);
  return settings?.join_policy || JOIN_POLICIES.NOT_ACCEPTING;
}

async function setGroupJoinPolicy(
  userId,
  groupId,
  joinPolicy,
  requestId,
  ip,
  userAgent
) {
  const resolvedGroupId = ensurePositiveInteger(groupId, "groupId");
  const resolvedJoinPolicy = ensureJoinPolicy(joinPolicy);

  return invitesModel.withTransaction(async (client) => {
    const { actorRole } = await ensureGroupAndManagerRole(
      userId,
      resolvedGroupId,
      client
    );
    await invitesModel.upsertGroupSettings(resolvedGroupId, resolvedJoinPolicy, client);

    await invitesModel.createGroupAuditLog(
      {
        groupId: resolvedGroupId,
        actorUserId: userId,
        actorRole,
        eventType: "GROUP_JOIN_POLICY_UPDATED",
        requestId,
        ip: normalizeIp(ip),
        userAgent: userAgent || null,
        metadata: {
          joinPolicy: resolvedJoinPolicy,
        },
      },
      client
    );
  });
}

module.exports = {
  InviteServiceError,
  JOIN_POLICIES,
  JOIN_RESULTS,
  acceptInviteLink,
  createInviteLink,
  deleteInviteLink,
  getGroupJoinPolicy,
  getInviteLinkSummaryByToken,
  listInviteLinks,
  resolveManagedGroupId,
  revokeInviteLink,
  reviveInviteLink,
  setGroupJoinPolicy,
};
