import { and, asc, eq, inArray, isNull } from 'drizzle-orm';
import { crmOpportunities, crmLeads } from '@/db/schema';
import { db } from '@/lib/db';
import { AuthError } from '@/lib/auth/session';
import { generateNextDocumentCode } from '@/features/foundation/document-sequence/service';
import { auditAction } from '@/features/foundation/audit-log/service';
import { getActiveOptionsByCategory } from '@/features/foundation/master-options/service';
import {
  canAccessScopedCrmRecord,
  type CrmSecurityContext
} from '@/features/crm/security/server/service';
import {
  assertAssignableUserBelongsToOrganization,
  assertContactBelongsToOrganization,
  assertCustomerBelongsToOrganization
} from '@/features/crm/opportunities/server/service';
import type { AssignLeadInput, AssignLeadResult } from '../types';

export type LeadAssignmentAccessContext = CrmSecurityContext;

type LeadRow = typeof crmLeads.$inferSelect;
type OpportunityRow = typeof crmOpportunities.$inferSelect;

function canAccessLeadRow(row: LeadRow, accessContext: LeadAssignmentAccessContext) {
  return canAccessScopedCrmRecord(accessContext, {
    branchId: row.branchId,
    createdBy: row.createdBy,
    ownerUserId: row.ownerMarketingUserId
  });
}

async function assertLeadBelongsToOrganization(
  leadId: string,
  organizationId: string,
  accessContext?: LeadAssignmentAccessContext
) {
  const [lead] = await db
    .select()
    .from(crmLeads)
    .where(
      and(
        eq(crmLeads.id, leadId),
        eq(crmLeads.organizationId, organizationId),
        isNull(crmLeads.deletedAt)
      )
    )
    .limit(1);

  if (!lead) {
    throw new AuthError('Lead not found', 404);
  }

  if (accessContext && !canAccessLeadRow(lead, accessContext)) {
    throw new AuthError('Forbidden', 403);
  }

  return lead;
}

async function resolveDefaultOpportunityStatusId(organizationId: string) {
  const options = await getActiveOptionsByCategory('crm_opportunity_status', { organizationId });
  const preferred = options.find((option) => option.code === 'new');
  const resolved = preferred ?? options[0];

  if (!resolved) {
    throw new AuthError('Opportunity status options are not configured', 400);
  }

  return resolved.id;
}

async function findActiveOpportunityByLead(leadId: string, organizationId: string) {
  const [opportunity] = await db
    .select()
    .from(crmOpportunities)
    .where(
      and(
        eq(crmOpportunities.leadId, leadId),
        eq(crmOpportunities.organizationId, organizationId),
        inArray(crmOpportunities.pipelineStage, ['lead', 'opportunity']),
        isNull(crmOpportunities.deletedAt)
      )
    )
    .orderBy(asc(crmOpportunities.createdAt))
    .limit(1);

  return opportunity ?? null;
}

async function syncLeadAssignment(input: {
  leadId: string;
  status: string;
  salesOwnerId: string;
  assignedBy: string;
  assignmentRemark?: string | null;
}) {
  const now = new Date();

  await db
    .update(crmLeads)
    .set({
      status: input.status,
      assignedSalesOwnerId: input.salesOwnerId,
      assignedAt: now,
      assignedBy: input.assignedBy,
      assignmentRemark: input.assignmentRemark?.trim() || null,
      updatedAt: now
    })
    .where(eq(crmLeads.id, input.leadId));
}

async function createOpportunityFromLead(input: {
  lead: LeadRow;
  organizationId: string;
  actorUserId: string;
  salesOwnerId: string;
  assignmentRemark?: string | null;
}) {
  if (!input.lead.customerId) {
    throw new AuthError('Lead requires a customer before assignment', 400);
  }

  if (!input.lead.productType) {
    throw new AuthError('Lead requires a product type before assignment', 400);
  }

  if (!input.lead.priority) {
    throw new AuthError('Lead requires a priority before assignment', 400);
  }

  await assertCustomerBelongsToOrganization(input.lead.customerId, input.organizationId);

  if (input.lead.contactId) {
    await assertContactBelongsToOrganization(
      input.lead.contactId,
      input.organizationId,
      input.lead.customerId
    );
  }

  const [statusId, documentCode] = await Promise.all([
    resolveDefaultOpportunityStatusId(input.organizationId),
    generateNextDocumentCode({
      organizationId: input.organizationId,
      documentType: 'crm_opportunity',
      branchId: input.lead.branchId ?? null
    })
  ]);

  const now = new Date();
  const [created] = await db
    .insert(crmOpportunities)
    .values({
      id: crypto.randomUUID(),
      organizationId: input.organizationId,
      branchId: input.lead.branchId ?? null,
      leadId: input.lead.id,
      code: documentCode.code,
      customerId: input.lead.customerId,
      contactId: input.lead.contactId ?? null,
      title: input.lead.projectName?.trim() || `Lead ${input.lead.code}`,
      description: input.lead.description?.trim() || null,
      requirement: null,
      projectName: input.lead.projectName?.trim() || null,
      projectLocation: input.lead.projectLocation?.trim() || null,
      productType: input.lead.productType,
      status: statusId,
      priority: input.lead.priority,
      leadChannel: null,
      estimatedValue: input.lead.estimatedValue ?? null,
      chancePercent: null,
      expectedCloseDate: null,
      competitor: null,
      source: `lead:${input.lead.code}`,
      notes: null,
      isHotProject: false,
      isActive: true,
      pipelineStage: 'opportunity',
      assignedToUserId: input.salesOwnerId,
      assignedAt: now,
      assignedBy: input.actorUserId,
      assignmentRemark: input.assignmentRemark?.trim() || null,
      createdBy: input.actorUserId,
      updatedBy: input.actorUserId
    })
    .returning();

  return created;
}

export async function assignLead(
  organizationId: string,
  actorUserId: string,
  input: AssignLeadInput,
  accessContext?: LeadAssignmentAccessContext
): Promise<AssignLeadResult> {
  const lead = await assertLeadBelongsToOrganization(input.leadId, organizationId, accessContext);
  await assertAssignableUserBelongsToOrganization(input.salesOwnerId, organizationId);

  const existingOpportunity = await findActiveOpportunityByLead(input.leadId, organizationId);

  if (existingOpportunity) {
    const resolvedSalesOwnerId = existingOpportunity.assignedToUserId ?? input.salesOwnerId;

    if (!existingOpportunity.assignedToUserId) {
      await db
        .update(crmOpportunities)
        .set({
          pipelineStage: 'opportunity',
          assignedToUserId: input.salesOwnerId,
          assignedAt: new Date(),
          assignedBy: actorUserId,
          assignmentRemark: input.assignmentRemark?.trim() || null,
          updatedAt: new Date(),
          updatedBy: actorUserId
        })
        .where(eq(crmOpportunities.id, existingOpportunity.id));
    }

    await syncLeadAssignment({
      leadId: input.leadId,
      status: 'assigned',
      salesOwnerId: resolvedSalesOwnerId,
      assignedBy: actorUserId,
      assignmentRemark: input.assignmentRemark
    });

    await auditAction({
      organizationId,
      branchId: lead.branchId,
      userId: actorUserId,
      entityType: 'crm_lead',
      entityId: input.leadId,
      action: 'assign_lead',
      afterData: {
        leadId: input.leadId,
        opportunityId: existingOpportunity.id,
        salesOwnerId: resolvedSalesOwnerId,
        reusedExistingOpportunity: true
      }
    });

    return {
      leadId: input.leadId,
      opportunityId: existingOpportunity.id,
      opportunityCode: existingOpportunity.code,
      reusedExistingOpportunity: true
    };
  }

  const createdOpportunity = await createOpportunityFromLead({
    lead,
    organizationId,
    actorUserId,
    salesOwnerId: input.salesOwnerId,
    assignmentRemark: input.assignmentRemark
  });

  await syncLeadAssignment({
    leadId: input.leadId,
    status: 'assigned',
    salesOwnerId: input.salesOwnerId,
    assignedBy: actorUserId,
    assignmentRemark: input.assignmentRemark
  });

  await auditAction({
    organizationId,
    branchId: lead.branchId,
    userId: actorUserId,
    entityType: 'crm_opportunity',
    entityId: createdOpportunity.id,
    action: 'create_opportunity_from_lead',
    afterData: {
      leadId: input.leadId,
      opportunityId: createdOpportunity.id,
      salesOwnerId: input.salesOwnerId
    }
  });

  await auditAction({
    organizationId,
    branchId: lead.branchId,
    userId: actorUserId,
    entityType: 'crm_lead',
    entityId: input.leadId,
    action: 'assign_lead',
    afterData: {
      leadId: input.leadId,
      opportunityId: createdOpportunity.id,
      salesOwnerId: input.salesOwnerId,
      reusedExistingOpportunity: false
    }
  });

  return {
    leadId: input.leadId,
    opportunityId: createdOpportunity.id,
    opportunityCode: createdOpportunity.code,
    reusedExistingOpportunity: false
  };
}

