/**
 * Pure, canonical IPv4 and IPv6 network prefixes using CIDR notation.
 *
 * @stability unstable
 * @since 4.0.0
 */
import * as Equal from "../Equal.ts"
import { dual } from "../Function.ts"
import * as Hash from "../Hash.ts"
import * as Inspectable from "../Inspectable.ts"
import { hasProperty } from "../Predicate.ts"
import * as Result from "../Result.ts"
import * as IpInterface from "./IpInterface.ts"
import * as NetAddress from "./NetAddress.ts"

const TypeId = "~effect/net/IpNetwork" as const

/**
 * An immutable canonical IP network prefix. Constructors that retain the exact
 * input address also retain its verified refinements. Operations that derive a
 * different address return only its IPv4 or IPv6 family; revalidate the result
 * before treating it as refined.
 *
 * @stability unstable
 * @category models
 * @since 4.0.0
 */
export interface IpNetwork<out A extends NetAddress.IpAddress = NetAddress.IpAddress>
  extends Equal.Equal, Hash.Hash, Inspectable.Inspectable
{
  readonly _tag: "IpNetwork"
  readonly address: A
  readonly prefixLength: number
  readonly [TypeId]: typeof TypeId
}

/**
 * An immutable canonical IPv4 network prefix.
 *
 * @stability unstable
 * @category models
 * @since 4.0.0
 */
export type Ipv4Network = IpNetwork<NetAddress.Ipv4Address>

/**
 * An immutable canonical IPv6 network prefix.
 *
 * @stability unstable
 * @category models
 * @since 4.0.0
 */
export type Ipv6Network = IpNetwork<NetAddress.Ipv6Address>

/**
 * A network address and prefix length, generic over the accepted address
 * input. The address must have no host bits set.
 *
 * @see {@link make} for the checked constructor
 * @stability unstable
 * @category models
 * @since 4.0.0
 */
export type IpNetworkParts<A extends NetAddress.IpAddressInput = NetAddress.IpAddressInput> =
  IpInterface.IpInterfaceParts<A>

/**
 * An IP network prefix, or a CIDR string or parts to convert to one.
 *
 * @stability unstable
 * @category models
 * @since 4.0.0
 */
export type IpNetworkInput = IpNetwork | string | IpNetworkParts

/**
 * An IPv4 network prefix, or a CIDR string or parts to convert to one.
 *
 * @stability unstable
 * @category models
 * @since 4.0.0
 */
export type Ipv4NetworkInput = Ipv4Network | string | IpNetworkParts<NetAddress.Ipv4AddressInput>

/**
 * An IPv6 network prefix, or a CIDR string or parts to convert to one.
 *
 * @stability unstable
 * @category models
 * @since 4.0.0
 */
export type Ipv6NetworkInput = Ipv6Network | string | IpNetworkParts<NetAddress.Ipv6AddressInput>

/**
 * Returns `true` when a value is an IPv4 network prefix.
 *
 * @stability unstable
 * @category guards
 * @since 4.0.0
 */
export const isIpv4Network = (u: unknown): u is Ipv4Network => isIpNetwork(u) && NetAddress.isIpv4Address(u.address)

/**
 * Returns `true` when a value is an IPv6 network prefix.
 *
 * @stability unstable
 * @category guards
 * @since 4.0.0
 */
export const isIpv6Network = (u: unknown): u is Ipv6Network => isIpNetwork(u) && NetAddress.isIpv6Address(u.address)

/**
 * Returns `true` when a value is an IPv4 or IPv6 network prefix.
 *
 * @stability unstable
 * @category guards
 * @since 4.0.0
 */
export const isIpNetwork = (u: unknown): u is IpNetwork => hasProperty(u, TypeId)

const IpNetworkProto = {
  ...Inspectable.BaseProto,
  _tag: "IpNetwork",
  [TypeId]: TypeId,
  [Equal.symbol](this: IpNetwork, that: Equal.Equal): boolean {
    return isIpNetwork(that) && this.prefixLength === that.prefixLength && Equal.equals(this.address, that.address)
  },
  [Hash.symbol](this: IpNetwork): number {
    return Hash.combine(Hash.hash(this.address), Hash.number(this.prefixLength))
  },
  toString(this: IpNetwork): string {
    return format(this)
  },
  toJSON(this: IpNetwork): string {
    return this.toString()
  }
}

const fromInterfaceValue = <A extends NetAddress.IpAddress>(value: IpInterface.IpInterface<A>): IpNetwork<A> => {
  const self = Object.assign(Object.create(IpNetworkProto), {
    address: value.address,
    prefixLength: value.prefixLength
  })
  return Object.freeze(self)
}

const toBytes = (address: NetAddress.IpAddress): ReadonlyArray<number> =>
  NetAddress.isIpv4Address(address) ? NetAddress.ipv4ToOctets(address) : NetAddress.ipv6ToOctets(address)

const fromBytes = <A extends NetAddress.IpAddress>(
  address: A,
  bytes: ReadonlyArray<number>
): NetAddress.Family<A> => {
  const array = new Uint8Array(bytes)
  return (NetAddress.isIpv4Address(address)
    ? NetAddress.ipv4FromBytesUnsafe(array)
    : NetAddress.ipv6FromBytesUnsafe(array)) as NetAddress.Family<A>
}

const maskBytes = (bytes: ReadonlyArray<number>, prefixLength: number): Array<number> => {
  const wholeBytes = Math.floor(prefixLength / 8)
  const partialBits = prefixLength % 8
  return bytes.map((byte, index) => {
    if (index < wholeBytes) return byte
    if (index === wholeBytes && partialBits !== 0) return byte & ((0xff << (8 - partialBits)) & 0xff)
    return 0
  })
}

/**
 * Creates a network prefix when the address has no host bits set.
 *
 * **Gotchas**
 *
 * Use {@link fromAddress} when a host address should be truncated to its containing network.
 *
 * @stability unstable
 * @category constructors
 * @since 4.0.0
 */
export const make = <A extends NetAddress.IpAddress>(
  address: A,
  prefixLength: number
): Result.Result<IpNetwork<A>, NetAddress.NetAddressError> => {
  return Result.flatMap(IpInterface.make(address, prefixLength), (value) => {
    const bytes = toBytes(address)
    const masked = maskBytes(bytes, prefixLength)
    if (bytes.some((byte, index) => byte !== masked[index])) {
      return Result.fail(
        new NetAddress.NetAddressError({ input: address, message: "address has non-zero host bits" })
      )
    }
    return Result.succeed(fromInterfaceValue(value))
  })
}

/**
 * Creates the canonical network containing an address by clearing its host bits.
 *
 * **Gotchas**
 *
 * The returned network's address can differ from the input address.
 * Its type therefore retains the IPv4 or IPv6 family but not refinements on
 * the input address. Revalidate the derived address to recover a refinement.
 *
 * @stability unstable
 * @category constructors
 * @since 4.0.0
 */
export const fromAddress = <A extends NetAddress.IpAddress>(
  address: A,
  prefixLength: number
): Result.Result<IpNetwork<NetAddress.Family<A>>, NetAddress.NetAddressError> => {
  return Result.map(IpInterface.make(address, prefixLength), (value) => {
    const masked = fromBytes(address, maskBytes(toBytes(address), prefixLength))
    return fromInterfaceValue(IpInterface.makeUnsafe(masked, value.prefixLength))
  })
}

/**
 * Returns the canonical network containing an interface address. The derived
 * address retains its IPv4 or IPv6 family but not refinements on the input.
 *
 * @stability unstable
 * @category conversions
 * @since 4.0.0
 */
export const fromInterface = <A extends NetAddress.IpAddress>(
  self: IpInterface.IpInterface<A>
): IpNetwork<NetAddress.Family<A>> => fromAddressUnsafe(self.address, self.prefixLength)

/**
 * Parses a strict IPv4 network prefix in CIDR notation.
 *
 * @stability unstable
 * @category decoding
 * @since 4.0.0
 */
export const ipv4FromString = (input: string): Result.Result<Ipv4Network, NetAddress.NetAddressError> => {
  return Result.flatMap(
    IpInterface.ipv4FromString(input, { prefix: "required" }),
    (value) => make(value.address, value.prefixLength)
  )
}

/**
 * Parses a strict IPv6 network prefix in CIDR notation.
 *
 * @stability unstable
 * @category decoding
 * @since 4.0.0
 */
export const ipv6FromString = (input: string): Result.Result<Ipv6Network, NetAddress.NetAddressError> => {
  return Result.flatMap(
    IpInterface.ipv6FromString(input, { prefix: "required" }),
    (value) => make(value.address, value.prefixLength)
  )
}

/**
 * Parses a strict IPv4 or IPv6 network prefix in CIDR notation.
 *
 * @stability unstable
 * @category decoding
 * @since 4.0.0
 */
export const fromString = (input: string): Result.Result<IpNetwork, NetAddress.NetAddressError> => {
  return Result.flatMap(
    IpInterface.fromString(input, { prefix: "required" }),
    (value) => make(value.address, value.prefixLength)
  )
}

const fromParts = <I extends NetAddress.IpAddressInput, A extends NetAddress.IpAddress>(
  input: IpNetworkParts<I>,
  toAddress: (input: I) => Result.Result<A, NetAddress.NetAddressError>
): Result.Result<IpNetwork<A>, NetAddress.NetAddressError> =>
  hasProperty(input, "address") && hasProperty(input, "prefixLength")
    ? Result.flatMap(toAddress(input.address), (address) => make(address, input.prefixLength))
    : Result.fail(new NetAddress.NetAddressError({ input, message: "expected an address and prefix length" }))

/**
 * Converts an `Ipv4NetworkInput` to an IPv4 network prefix, parsing strings
 * like `ipv4FromString`, converting parts like `make`, and returning network
 * prefixes unchanged.
 *
 * @stability unstable
 * @category constructors
 * @since 4.0.0
 */
export const ipv4FromInput = (input: Ipv4NetworkInput): Result.Result<Ipv4Network, NetAddress.NetAddressError> => {
  if (isIpv4Network(input)) return Result.succeed(input)
  if (typeof input === "string") return ipv4FromString(input)
  return fromParts(input, NetAddress.ipv4FromInput)
}

/**
 * Converts an `Ipv6NetworkInput` to an IPv6 network prefix, parsing strings
 * like `ipv6FromString`, converting parts like `make`, and returning network
 * prefixes unchanged.
 *
 * @stability unstable
 * @category constructors
 * @since 4.0.0
 */
export const ipv6FromInput = (input: Ipv6NetworkInput): Result.Result<Ipv6Network, NetAddress.NetAddressError> => {
  if (isIpv6Network(input)) return Result.succeed(input)
  if (typeof input === "string") return ipv6FromString(input)
  return fromParts(input, NetAddress.ipv6FromInput)
}

/**
 * Converts an `IpNetworkInput` to a network prefix, parsing strings like
 * `fromString`, converting parts like `make`, and returning network prefixes
 * unchanged.
 *
 * **Gotchas**
 *
 * Like `make`, an address with host bits set is rejected rather than truncated.
 *
 * @stability unstable
 * @category constructors
 * @since 4.0.0
 */
export const fromInput = (input: IpNetworkInput): Result.Result<IpNetwork, NetAddress.NetAddressError> => {
  if (isIpNetwork(input)) return Result.succeed(input)
  if (typeof input === "string") return fromString(input)
  return fromParts(input, NetAddress.ipFromInput)
}

/**
 * Converts a trusted `IpNetworkInput` to a network prefix, throwing on failure.
 *
 * @stability unstable
 * @category unsafe
 * @since 4.0.0
 */
export const fromInputUnsafe = (input: IpNetworkInput): IpNetwork => Result.getOrThrow(fromInput(input))

/**
 * Creates a trusted network prefix, throwing when its address or prefix is invalid.
 *
 * @stability unstable
 * @category unsafe
 * @since 4.0.0
 */
export const makeUnsafe = <A extends NetAddress.IpAddress>(address: A, prefixLength: number): IpNetwork<A> =>
  Result.getOrThrow(make(address, prefixLength))

/**
 * Creates the network containing a trusted address, throwing when its prefix is invalid.
 * The derived address retains its IPv4 or IPv6 family but not refinements on
 * the input address.
 *
 * @stability unstable
 * @category unsafe
 * @since 4.0.0
 */
export const fromAddressUnsafe = <A extends NetAddress.IpAddress>(
  address: A,
  prefixLength: number
): IpNetwork<NetAddress.Family<A>> => Result.getOrThrow(fromAddress(address, prefixLength))

/**
 * Parses a trusted network prefix in CIDR notation, throwing on failure.
 *
 * @stability unstable
 * @category unsafe
 * @since 4.0.0
 */
export const fromStringUnsafe = (input: string): IpNetwork => Result.getOrThrow(fromString(input))

/**
 * Formats a network prefix using canonical address text and its decimal prefix length.
 *
 * @stability unstable
 * @category encoding
 * @since 4.0.0
 */
export const format = (self: IpNetwork): string => `${NetAddress.formatIp(self.address)}/${self.prefixLength}`

/**
 * Returns the lowest address in a network prefix.
 *
 * @stability unstable
 * @category getters
 * @since 4.0.0
 */
export const firstAddress = <A extends NetAddress.IpAddress>(self: IpNetwork<A>): A => self.address

/**
 * Returns the numerically greatest address in a network prefix. The derived
 * address retains its IPv4 or IPv6 family but not refinements on the network
 * address.
 *
 * @stability unstable
 * @category getters
 * @since 4.0.0
 */
export const lastAddress = <A extends NetAddress.IpAddress>(self: IpNetwork<A>): NetAddress.Family<A> => {
  const bytes = toBytes(self.address)
  const networkMask = maskBytes(bytes.map(() => 0xff), self.prefixLength)
  return fromBytes(self.address, bytes.map((byte, index) => byte | (networkMask[index] ^ 0xff)))
}

/**
 * Returns the exact number of addresses in a network prefix.
 *
 * @stability unstable
 * @category getters
 * @since 4.0.0
 */
export const addressCount = (self: IpNetwork): bigint =>
  BigInt(1) << BigInt(NetAddress.width(self.address) - self.prefixLength)

const containsBytes = (self: IpNetwork, address: NetAddress.IpAddress): boolean => {
  if (NetAddress.isIpv4Address(self.address) !== NetAddress.isIpv4Address(address)) return false
  const expected = toBytes(self.address)
  const actual = maskBytes(toBytes(address), self.prefixLength)
  return expected.every((byte, index) => byte === actual[index])
}

/**
 * Returns `true` when a network prefix contains an IP address.
 *
 * @stability unstable
 * @category predicates
 * @since 4.0.0
 */
export const contains: {
  /**
   * Returns `true` when a network prefix contains an IP address.
   *
   * @stability unstable
   * @category predicates
   * @since 4.0.0
   */
  (address: NetAddress.IpAddress): (self: IpNetwork) => boolean
  /**
   * Returns `true` when a network prefix contains an IP address.
   *
   * @stability unstable
   * @category predicates
   * @since 4.0.0
   */
  (self: IpNetwork, address: NetAddress.IpAddress): boolean
} = dual(2, containsBytes)

/**
 * Returns `true` when a network prefix contains every address in another network prefix.
 *
 * @stability unstable
 * @category predicates
 * @since 4.0.0
 */
export const containsNetwork: {
  /**
   * Returns `true` when a network prefix contains every address in another network prefix.
   *
   * @stability unstable
   * @category predicates
   * @since 4.0.0
   */
  (other: IpNetwork): (self: IpNetwork) => boolean
  /**
   * Returns `true` when a network prefix contains every address in another network prefix.
   *
   * @stability unstable
   * @category predicates
   * @since 4.0.0
   */
  (self: IpNetwork, other: IpNetwork): boolean
} = dual(2, (self: IpNetwork, other: IpNetwork): boolean => {
  return self.prefixLength <= other.prefixLength && containsBytes(self, other.address)
})

/**
 * Returns `true` when two network prefixes contain at least one common address.
 *
 * @stability unstable
 * @category predicates
 * @since 4.0.0
 */
export const overlaps: {
  /**
   * Returns `true` when two network prefixes contain at least one common address.
   *
   * @stability unstable
   * @category predicates
   * @since 4.0.0
   */
  (other: IpNetwork): (self: IpNetwork) => boolean
  /**
   * Returns `true` when two network prefixes contain at least one common address.
   *
   * @stability unstable
   * @category predicates
   * @since 4.0.0
   */
  (self: IpNetwork, other: IpNetwork): boolean
} = dual(2, (self: IpNetwork, other: IpNetwork): boolean => {
  return self.prefixLength <= other.prefixLength
    ? containsBytes(self, other.address)
    : containsBytes(other, self.address)
})
