import { and, eq } from 'drizzle-orm';
import { Hono } from 'hono';
import { z } from 'zod';
import {
  ACCOUNT_SIDES,
  CONTRACT_STATUSES,
  CONTRACT_TYPES,
  SLA_KINDS,
  SLA_METRICS,
} from '@pig/core';
import type { AccountSide } from '@pig/core';
import type { Contract, Database } from '@pig/db';
import {
  accounts,
  capacityCommitments,
  contractObligations,
  contracts,
  demandDeals,
  slaMetricTargets,
  slaTerms,
  supplyDeals,
} from '@pig/db';
import { effectivePermissions, requireCapability, type Principal } from '../lib/auth';
import {
  MutationError,
  mutation,
  type ApiEnv,
  type MutationDefinition,
} from '../lib/mutation';
import {
  ContractService,
  validateParentRelationship,
} from '../services/contracts';
import { permissionGranted } from '@pig/core';

const dateValue = z.string().datetime().nullable();
const percentage = z.number().min(0).max(100).nullable();
const nullableId = z.string().uuid().nullable();

const maintenanceClassSchema = z.object({
  class: z.string().min(1).max(80),
  noticeValue: z.number().int().nonnegative(),
  noticeUnit: z.string().min(1).max(40),
  allowancePerPeriodHours: z.number().nonnegative().optional(),
  excludedFromUptime: z.boolean(),
});

const slaInputSchema = z.object({
  kind: z.enum(SLA_KINDS),
  uptimeTargetPct: z.number().min(0).max(100).nullable().optional(),
  nodeReplacementHours: z.number().int().nonnegative().nullable().optional(),
  mttrHours: z.number().int().nonnegative().nullable().optional(),
  supportResponseHours: z.number().int().nonnegative().nullable().optional(),
  measurementWindow: z.string().min(1).max(80).optional(),
  measurementUnit: z.string().min(1).max(80).optional(),
  remedyType: z.enum(['service_credit', 'fee_abatement', 'termination_right']).optional(),
  abatementTriggerValue: z.number().int().positive().nullable().optional(),
  abatementTriggerUnit: z.string().min(1).max(40).nullable().optional(),
  claimDeadlineValue: z.number().int().positive().nullable().optional(),
  claimDeadlineUnit: z.string().min(1).max(40).optional(),
  creditExpiryMonths: z.number().int().positive().nullable().optional(),
  isSoleRemedy: z.boolean().optional(),
  sparePoolObligation: z.string().max(4000).nullable().optional(),
  sparePoolScope: z.array(z.string().min(1).max(120)).max(30).optional(),
  maintenanceClasses: z.array(maintenanceClassSchema).max(20).optional(),
  reasonableEndeavoursDaysPerYear: z.number().int().min(0).max(366).nullable().optional(),
  rcaDeliveryHours: z.number().int().positive().nullable().optional(),
  creditSchedule: z
    .array(
      z.object({
        belowPct: z.number().min(0).max(100),
        creditPct: z.number().min(0).max(100),
      }),
    )
    .max(20)
    .optional(),
  creditCapPct: z.number().min(0).max(100).nullable().optional(),
  exclusions: z.string().max(8000).nullable().optional(),
  metricTargets: z
    .array(
      z.object({
        metric: z.enum(SLA_METRICS),
        targetValue: z.number(),
        unit: z.string().max(80).nullable().optional(),
      }),
    )
    .max(20)
    .optional(),
});

const contractFields = {
  type: z.enum(CONTRACT_TYPES).optional(),
  status: z.enum(CONTRACT_STATUSES).optional(),
  side: z.enum(ACCOUNT_SIDES).optional(),
  title: z.string().min(1).max(240).optional(),
  externalReference: z.string().max(240).nullable().optional(),
  demandDealId: nullableId.optional(),
  supplyDealId: nullableId.optional(),
  capacityCommitmentId: nullableId.optional(),
  parentContractId: nullableId.optional(),
  contractingPartyName: z.string().max(240).nullable().optional(),
  takeOrPayFloorPct: percentage.optional(),
  prepaidPct: percentage.optional(),
  terminationTier: z
    .enum(['1_prepaid', '2_take_or_pay', '3_cancellable'])
    .nullable()
    .optional(),
  assignableOnDefault: z.boolean().optional(),
  assignmentDeadlineBusinessDays: z.number().int().positive().nullable().optional(),
  effectiveAt: dateValue.optional(),
  expiresAt: dateValue.optional(),
  executedAt: dateValue.optional(),
  terminatedAt: dateValue.optional(),
  isAutoRenew: z.boolean().optional(),
  noticeDays: z.number().int().positive().nullable().optional(),
  valueCents: z.number().int().nonnegative().nullable().optional(),
  currency: z.string().length(3).optional(),
  governingLaw: z.string().max(240).nullable().optional(),
  documentUrl: z.string().url().max(2000).nullable().optional(),
  ownerUserId: nullableId.optional(),
  notes: z.string().max(12000).nullable().optional(),
  sla: slaInputSchema.nullable().optional(),
};

const updateContractSchema = z.object(contractFields);
const createContractSchema = updateContractSchema.extend({
  accountId: z.string().uuid(),
  type: z.enum(CONTRACT_TYPES),
  side: z.enum(ACCOUNT_SIDES),
  title: z.string().min(1).max(240),
});

const obligationFields = {
  title: z.string().min(1).max(240).optional(),
  description: z.string().max(8000).nullable().optional(),
  kind: z.enum(['renewal_notice', 'milestone', 'payment', 'review', 'true_up']).optional(),
  dueAt: z.string().datetime().optional(),
  completedAt: dateValue.optional(),
  ownerUserId: nullableId.optional(),
};
const createObligationSchema = z.object({
  ...obligationFields,
  title: z.string().min(1).max(240),
  dueAt: z.string().datetime(),
});
const updateObligationSchema = z.object(obligationFields);

type Transaction = Parameters<Parameters<Database['transaction']>[0]>[0];

function writtenRow<Row>(row: Row | undefined, resource: string): Row {
  if (row === undefined) {
    throw new Error(`${resource} write completed without returning a row.`);
  }
  return row;
}

function requiredRouteParam(
  params: Readonly<Record<string, string>>,
  name: 'id' | 'obligationId',
): string {
  const value = params[name];
  if (!value) {
    throw new MutationError(
      'invalid_route_parameter',
      `Route parameter '${name}' is required.`,
      400,
    );
  }
  return value;
}

function requireAnyContractPermission(principal: Principal): void {
  const grants = effectivePermissions(principal);
  if (
    permissionGranted(grants, 'contract:sign', 'supply') ||
    permissionGranted(grants, 'contract:sign', 'demand')
  ) {
    return;
  }
  requireCapability(principal, 'contract:sign', 'demand');
}

function ensureSidePermission(principal: Principal, side: AccountSide): void {
  if (side === 'both') {
    throw new MutationError(
      'invalid_contract_side',
      'A contract must govern either the supply or demand side.',
      400,
    );
  }
  requireCapability(principal, 'contract:sign', side);
}

function supportsSide(accountSide: AccountSide, contractSide: AccountSide): boolean {
  return accountSide === 'both' || accountSide === contractSide;
}

async function requireRelationships(
  tx: Transaction,
  record: Pick<
    Contract,
    | 'id'
    | 'accountId'
    | 'side'
    | 'parentContractId'
    | 'demandDealId'
    | 'supplyDealId'
    | 'capacityCommitmentId'
  >,
): Promise<void> {
  const [account] = await tx.select().from(accounts).where(eq(accounts.id, record.accountId)).limit(1);
  if (!account) throw MutationError.notFound('Account');
  if (!supportsSide(account.side, record.side as AccountSide)) {
    throw new MutationError(
      'side_mismatch',
      `This account is not recorded on the ${record.side} side.`,
      409,
    );
  }

  if (record.parentContractId) {
    const [parent] = await tx
      .select()
      .from(contracts)
      .where(eq(contracts.id, record.parentContractId))
      .limit(1);
    if (!parent) throw MutationError.notFound('Parent contract');

    const ancestors: Contract[] = [];
    let cursor = parent;
    while (cursor.parentContractId) {
      const [next] = await tx
        .select()
        .from(contracts)
        .where(eq(contracts.id, cursor.parentContractId))
        .limit(1);
      if (!next) throw MutationError.notFound('Parent contract');
      ancestors.push(next);
      if (ancestors.length > 100) {
        throw new MutationError('invalid_hierarchy', 'Contract hierarchy is too deep.', 409);
      }
      cursor = next;
    }
    const issue = validateParentRelationship(record, parent, ancestors);
    if (issue) throw new MutationError('invalid_hierarchy', issue, 409);
  }

  if (record.demandDealId) {
    const [deal] = await tx
      .select()
      .from(demandDeals)
      .where(eq(demandDeals.id, record.demandDealId))
      .limit(1);
    if (!deal) throw MutationError.notFound('Demand deal');
    if (record.side !== 'demand' || deal.accountId !== record.accountId) {
      throw new MutationError('relationship_mismatch', 'Demand deal does not match this contract.', 409);
    }
  }
  if (record.supplyDealId) {
    const [deal] = await tx
      .select()
      .from(supplyDeals)
      .where(eq(supplyDeals.id, record.supplyDealId))
      .limit(1);
    if (!deal) throw MutationError.notFound('Supply deal');
    if (record.side !== 'supply' || deal.accountId !== record.accountId) {
      throw new MutationError('relationship_mismatch', 'Supply deal does not match this contract.', 409);
    }
  }
  if (record.capacityCommitmentId) {
    const [commitment] = await tx
      .select()
      .from(capacityCommitments)
      .where(eq(capacityCommitments.id, record.capacityCommitmentId))
      .limit(1);
    if (!commitment) throw MutationError.notFound('Capacity commitment');
    if (record.side !== 'supply' || commitment.accountId !== record.accountId) {
      throw new MutationError(
        'relationship_mismatch',
        'Capacity commitment does not match this contract.',
        409,
      );
    }
  }
}

function date(value: string | null | undefined): Date | null | undefined {
  return value === undefined ? undefined : value === null ? null : new Date(value);
}

function contractValues(input: z.infer<typeof updateContractSchema>) {
  const { sla: _sla, ...plain } = input;
  return {
    ...plain,
    takeOrPayFloorPct:
      plain.takeOrPayFloorPct === undefined || plain.takeOrPayFloorPct === null
        ? plain.takeOrPayFloorPct
        : String(plain.takeOrPayFloorPct),
    prepaidPct:
      plain.prepaidPct === undefined || plain.prepaidPct === null
        ? plain.prepaidPct
        : String(plain.prepaidPct),
    effectiveAt: date(plain.effectiveAt),
    expiresAt: date(plain.expiresAt),
    executedAt: date(plain.executedAt),
    terminatedAt: date(plain.terminatedAt),
  };
}

async function writeSla(
  tx: Transaction,
  contractId: string,
  input: z.infer<typeof slaInputSchema> | null | undefined,
  now: Date,
): Promise<void> {
  if (input === undefined) return;
  const existing = await tx.select().from(slaTerms).where(eq(slaTerms.contractId, contractId));
  if (input === null) {
    if (existing.length) await tx.delete(slaTerms).where(eq(slaTerms.contractId, contractId));
    return;
  }

  const { metricTargets, ...values } = input;
  const stored = {
    ...values,
    uptimeTargetPct:
      values.uptimeTargetPct == null ? values.uptimeTargetPct : String(values.uptimeTargetPct),
    creditCapPct:
      values.creditCapPct == null ? values.creditCapPct : String(values.creditCapPct),
    updatedAt: now,
  };
  const existingTerm = existing[0];
  const writtenTerms = existingTerm
    ? await tx.update(slaTerms).set(stored).where(eq(slaTerms.id, existingTerm.id)).returning()
    : await tx.insert(slaTerms).values({ contractId, ...stored }).returning();
  const term = writtenRow(writtenTerms[0], 'SLA term');

  if (metricTargets !== undefined) {
    await tx.delete(slaMetricTargets).where(eq(slaMetricTargets.slaTermId, term.id));
    if (metricTargets.length) {
      await tx.insert(slaMetricTargets).values(
        metricTargets.map((target) => ({
          slaTermId: term.id,
          metric: target.metric,
          targetValue: String(target.targetValue),
          unit: target.unit,
        })),
      );
    }
  }
}

export function createContractMutationDefinition(): MutationDefinition<
  typeof createContractSchema,
  Contract
> {
  return {
    schema: createContractSchema,
    permission: { authorize: requireAnyContractPermission },
    invalidMessage: 'Invalid contract.',
    async mutate({ input, principal, tx, now }) {
      ensureSidePermission(principal, input.side);
      const { sla, accountId, type, side, title, ...fields } = input;
      const pending = {
        id: crypto.randomUUID(),
        accountId,
        type,
        side,
        title,
        ...contractValues(fields),
      } as Contract;
      await requireRelationships(tx, pending);
      const createdRows = await tx
        .insert(contracts)
        .values({ accountId, type, side, title, ...contractValues(fields), updatedAt: now })
        .returning();
      const created = writtenRow(createdRows[0], 'Contract');
      await writeSla(tx, created.id, sla, now);
      return {
        data: created,
        activity: {
          type: 'contract_event',
          subject: `Created ${created.title}`,
          accountId: created.accountId,
          meta: { contractId: created.id, status: created.status, contractType: created.type },
        },
      };
    },
  };
}

export function updateContractMutationDefinition(): MutationDefinition<
  typeof updateContractSchema,
  Contract
> {
  return {
    schema: updateContractSchema,
    permission: { authorize: requireAnyContractPermission },
    invalidMessage: 'Invalid contract update.',
    async mutate({ input, principal, params, tx, now }) {
      const contractId = requiredRouteParam(params, 'id');
      const [before] = await tx.select().from(contracts).where(eq(contracts.id, contractId)).limit(1);
      if (!before) throw MutationError.notFound('Contract');
      const { sla, ...fields } = input;
      const pending = { ...before, ...contractValues(fields), updatedAt: now };
      ensureSidePermission(principal, pending.side as AccountSide);
      await requireRelationships(tx, pending);
      const updatedRows = await tx
        .update(contracts)
        .set({ ...contractValues(fields), updatedAt: now })
        .where(eq(contracts.id, before.id))
        .returning();
      const updated = writtenRow(updatedRows[0], 'Contract');
      await writeSla(tx, updated.id, sla, now);
      return {
        data: updated,
        activity: {
          type: 'contract_event',
          subject: `Updated ${updated.title}`,
          accountId: updated.accountId,
          meta: {
            contractId: updated.id,
            fromStatus: before.status,
            toStatus: updated.status,
          },
        },
      };
    },
  };
}

export function createObligationMutationDefinition(): MutationDefinition<
  typeof createObligationSchema,
  typeof contractObligations.$inferSelect
> {
  return {
    schema: createObligationSchema,
    permission: { authorize: requireAnyContractPermission },
    invalidMessage: 'Invalid contract obligation.',
    async mutate({ input, principal, params, tx, now }) {
      const contractId = requiredRouteParam(params, 'id');
      const [contract] = await tx.select().from(contracts).where(eq(contracts.id, contractId)).limit(1);
      if (!contract) throw MutationError.notFound('Contract');
      ensureSidePermission(principal, contract.side as AccountSide);
      const createdRows = await tx
        .insert(contractObligations)
        .values({
          ...input,
          contractId: contract.id,
          dueAt: new Date(input.dueAt),
          completedAt: date(input.completedAt),
          updatedAt: now,
        })
        .returning();
      const created = writtenRow(createdRows[0], 'Contract obligation');
      return {
        data: created,
        activity: {
          type: 'task',
          subject: `Added obligation: ${created.title}`,
          accountId: contract.accountId,
          meta: { contractId: contract.id, obligationId: created.id, dueAt: created.dueAt },
        },
      };
    },
  };
}

export function updateObligationMutationDefinition(): MutationDefinition<
  typeof updateObligationSchema,
  typeof contractObligations.$inferSelect
> {
  return {
    schema: updateObligationSchema,
    permission: { authorize: requireAnyContractPermission },
    invalidMessage: 'Invalid contract obligation update.',
    async mutate({ input, principal, params, tx, now }) {
      const contractId = requiredRouteParam(params, 'id');
      const obligationId = requiredRouteParam(params, 'obligationId');
      const [before] = await tx
        .select({ obligation: contractObligations, contract: contracts })
        .from(contractObligations)
        .innerJoin(contracts, eq(contracts.id, contractObligations.contractId))
        .where(
          and(
            eq(contractObligations.id, obligationId),
            eq(contractObligations.contractId, contractId),
          ),
        )
        .limit(1);
      if (!before) throw MutationError.notFound('Contract obligation');
      ensureSidePermission(principal, before.contract.side as AccountSide);
      const updatedRows = await tx
        .update(contractObligations)
        .set({
          ...input,
          dueAt: input.dueAt ? new Date(input.dueAt) : undefined,
          completedAt: date(input.completedAt),
          updatedAt: now,
        })
        .where(eq(contractObligations.id, before.obligation.id))
        .returning();
      const updated = writtenRow(updatedRows[0], 'Contract obligation');
      return {
        data: updated,
        activity: {
          type: 'task',
          subject: `${updated.completedAt ? 'Completed' : 'Updated'} obligation: ${updated.title}`,
          accountId: before.contract.accountId,
          meta: {
            contractId: before.contract.id,
            obligationId: updated.id,
            completedAt: updated.completedAt,
          },
        },
      };
    },
  };
}

export function createContractRoutes(db: Database) {
  const routes = new Hono<ApiEnv>();
  const service = new ContractService(db);

  routes.get('/api/contracts', async (c) => c.json(await service.list()));
  routes.get('/api/contracts/:id', async (c) => {
    const detail = await service.detail(c.req.param('id'));
    return detail ? c.json(detail) : c.json({ error: 'Contract not found.' }, 404);
  });
  routes.post('/api/contracts', mutation(db, createContractMutationDefinition()));
  routes.patch('/api/contracts/:id', mutation(db, updateContractMutationDefinition()));
  routes.post(
    '/api/contracts/:id/obligations',
    mutation(db, createObligationMutationDefinition()),
  );
  routes.patch(
    '/api/contracts/:id/obligations/:obligationId',
    mutation(db, updateObligationMutationDefinition()),
  );
  return routes;
}
