import { Effect, Schema } from "effect";

const MeaningfulString = Schema.String.check(
  Schema.makeFilter((value) => value.trim().length > 0, {
    expected: "a non-empty string",
  })
);

export const SignalId = MeaningfulString.pipe(Schema.brand("SignalId"));
export type SignalId = typeof SignalId.Type;

export const OrganizationId = MeaningfulString.pipe(
  Schema.brand("OrganizationId")
);
export type OrganizationId = typeof OrganizationId.Type;

export const ProjectId = MeaningfulString.pipe(Schema.brand("ProjectId"));
export type ProjectId = typeof ProjectId.Type;

export const ConversationId = MeaningfulString.pipe(
  Schema.brand("ConversationId")
);
export type ConversationId = typeof ConversationId.Type;

export const ConversationMessageId = MeaningfulString.pipe(
  Schema.brand("ConversationMessageId")
);
export type ConversationMessageId = typeof ConversationMessageId.Type;

export const TimestampMs = Schema.Int.check(
  Schema.isGreaterThanOrEqualTo(0)
).pipe(Schema.brand("TimestampMs"));
export type TimestampMs = typeof TimestampMs.Type;

export const OrganizationSignalScope = Schema.Struct({
  _tag: Schema.Literal("Organization"),
  organizationId: OrganizationId,
});
export type OrganizationSignalScope = typeof OrganizationSignalScope.Type;

export const ProjectSignalScope = Schema.Struct({
  _tag: Schema.Literal("Project"),
  organizationId: OrganizationId,
  projectId: ProjectId,
});
export type ProjectSignalScope = typeof ProjectSignalScope.Type;

export const SignalScope = Schema.Union([
  OrganizationSignalScope,
  ProjectSignalScope,
]);
export type SignalScope = typeof SignalScope.Type;

export const SignalSourceMessage = Schema.Struct({
  conversationId: ConversationId,
  createdAt: TimestampMs,
  messageId: ConversationMessageId,
  rawText: Schema.String,
});
export type SignalSourceMessage = typeof SignalSourceMessage.Type;

export const ProblemStatement = Schema.Struct({
  constraints: Schema.Array(MeaningfulString),
  desiredOutcome: MeaningfulString,
  summary: MeaningfulString,
  title: MeaningfulString,
});
export type ProblemStatement = typeof ProblemStatement.Type;

export const ProcessedByAgent = Schema.Struct({
  agentInstanceId: MeaningfulString,
  agentName: MeaningfulString,
});
export type ProcessedByAgent = typeof ProcessedByAgent.Type;

const signalFields = {
  createdAt: TimestampMs,
  id: SignalId,
  problemStatement: ProblemStatement,
  processedBy: ProcessedByAgent,
  scope: SignalScope,
  sourceMessages: Schema.NonEmptyArray(SignalSourceMessage),
} as const;

export const SignalInput = Schema.Struct(signalFields);
export type SignalInput = typeof SignalInput.Type;

export const Signal = Schema.Struct(signalFields);
export type Signal = typeof Signal.Type;

export const SignalValidationReason = Schema.Literals([
  "InvalidInput",
  "DuplicateSource",
  "MixedConversation",
]);
export type SignalValidationReason = typeof SignalValidationReason.Type;

export class SignalValidationError extends Schema.TaggedErrorClass<SignalValidationError>()(
  "SignalValidationError",
  {
    message: Schema.String,
    reason: SignalValidationReason,
  }
) {}

export const composeSignal = Effect.fn("Signal.compose")(
  function* composeSignal(input: unknown) {
    const signal: Signal = yield* Schema.decodeUnknownEffect(SignalInput)(
      input
    ).pipe(
      Effect.mapError(
        (cause) =>
          new SignalValidationError({
            message: cause.message,
            reason: "InvalidInput",
          })
      )
    );

    // Source order follows the agent's logical selection, not chronological
    // timestamp order. Each source retains its original createdAt so temporal
    // provenance is preserved exactly; the ordinal array position reflects the
    // agent's narrative composition of the problem statement.
    const [{ conversationId }] = signal.sourceMessages;
    const messageIds = new Set<ConversationMessageId>();

    for (const source of signal.sourceMessages) {
      if (source.conversationId !== conversationId) {
        return yield* Effect.fail(
          new SignalValidationError({
            message: "Signal sources must belong to one conversation",
            reason: "MixedConversation",
          })
        );
      }

      if (messageIds.has(source.messageId)) {
        return yield* Effect.fail(
          new SignalValidationError({
            message: "Signal sources must have unique message identities",
            reason: "DuplicateSource",
          })
        );
      }
      messageIds.add(source.messageId);
    }

    return signal;
  }
);
