# frozen_string_literal: true

module Puppet
  Puppet::Type.type(:file).ensurable do
    require 'etc'
    require_relative '../../../puppet/util/symbolic_file_mode'
    include Puppet::Util::SymbolicFileMode

    desc <<-EOT
      Whether the file should exist, and if so what kind of file it should be.
      Possible values are `present`, `absent`, `file`, `directory`, and `link`.

      * `present` accepts any form of file existence, and creates a
        normal file if the file is missing. (The file will have no content
        unless the `content` or `source` attribute is used.)
      * `absent` ensures the file doesn't exist, and deletes it if necessary.
      * `file` ensures it's a normal file, and enables use of the `content` or
        `source` attribute.
      * `directory` ensures it's a directory, and enables use of the `source`,
        `recurse`, `recurselimit`, `ignore`, and `purge` attributes.
      * `link` ensures the file is a symlink, and **requires** that you also
        set the `target` attribute. Symlinks are supported on all Posix
        systems and on Windows Vista / 2008 and higher. On Windows, managing
        symlinks requires Puppet agent's user account to have the "Create
        Symbolic Links" privilege; this can be configured in the "User Rights
        Assignment" section in the Windows policy editor. By default, Puppet
        agent runs as the Administrator account, which has this privilege.

      Puppet avoids destroying directories unless the `force` attribute is set
      to `true`. This means that if a file is currently a directory, setting
      `ensure` to anything but `directory` or `present` will cause Puppet to
      skip managing the resource and log either a notice or an error.

      There is one other non-standard value for `ensure`. If you specify the
      path to another file as the ensure value, it is equivalent to specifying
      `link` and using that path as the `target`:

          # Equivalent resources:

          file { '/etc/inetd.conf':
            ensure => '/etc/inet/inetd.conf',
          }

          file { '/etc/inetd.conf':
            ensure => link,
            target => '/etc/inet/inetd.conf',
          }

      However, we recommend using `link` and `target` explicitly, since this
      behavior can be harder to read and is
      [deprecated](https://docs.puppet.com/puppet/4.3/deprecated_language.html)
      as of Puppet 4.3.0.
    EOT

    # Most 'ensure' properties have a default, but with files we, um, don't.
    nodefault

    newvalue(:absent) do
      Puppet::FileSystem.unlink(@resource[:path])
    end

    aliasvalue(:false, :absent)

    newvalue(:file, :event => :file_created) do
      # Make sure we're not managing the content some other way
      property = @resource.property(:content) || @resource.property(:checksum_value)
      if property
        property.sync
      else
        @resource.write
        @resource.should(:mode)
      end
    end

    # aliasvalue(:present, :file)
    newvalue(:present, :event => :file_created) do
      # Make a file if they want something, but this will match almost
      # anything.
      set_file
    end

    newvalue(:directory, :event => :directory_created) do
      mode = @resource.should(:mode)
      parent = File.dirname(@resource[:path])
      unless Puppet::FileSystem.exist? parent
        raise Puppet::Error,
              "Cannot create #{@resource[:path]}; parent directory #{parent} does not exist"
      end
      begin
        if mode
          Puppet::Util.withumask(0o00) do
            Dir.mkdir(@resource[:path], symbolic_mode_to_int(mode, 0o755, true))
          end
        else
          Dir.mkdir(@resource[:path])
        end
      rescue Errno::EEXIST
        # Another process may have created the directory concurrently. If it
        # really is a directory now, treat that as success.
        #
        # Directory mode, and other properties, will be checked and enforced by
        # the call to `property_fix`.
        raise unless Puppet::FileSystem.directory?(@resource[:path])
      end
      @resource.send(:property_fix)
      return :directory_created
    end

    newvalue(:link, :event => :link_created, :required_features => :manages_symlinks) do
      property = resource.property(:target)
      fail "Cannot create a symlink without a target" unless property

      property.retrieve
      property.mklink
    end

    # Symlinks.
    newvalue(/./) do
      # This code never gets executed.  We need the regex to support
      # specifying it, but the work is done in the 'symlink' code block.
    end

    munge do |value|
      value = super(value)
      unless value.is_a? Symbol
        resource[:target] = value
        value = :link
      end
      resource[:links] = :manage if value == :link and resource[:links] != :follow
      value
    end

    def change_to_s(currentvalue, newvalue)
      return super unless [:file, :present].include?(newvalue)

      property = @resource.property(:content)
      return super unless property

      # We know that content is out of sync if we're here, because
      # it's essentially equivalent to 'ensure' in the transaction.
      source = @resource.parameter(:source)
      if source
        should = source.checksum
      else
        should = property.should
      end
      if should == :absent
        is = property.retrieve
      else
        is = :absent
      end

      property.change_to_s(is, should)
    end

    # Check that we can actually create anything
    def check
      basedir = File.dirname(@resource[:path])

      if !Puppet::FileSystem.exist?(basedir)
        raise Puppet::Error,
              "Can not create #{@resource.title}; parent directory does not exist"
      elsif !FileTest.directory?(basedir)
        raise Puppet::Error,
              "Can not create #{@resource.title}; #{dirname} is not a directory"
      end
    end

    # We have to treat :present specially, because it works with any
    # type of file.
    def insync?(currentvalue)
      unless currentvalue == :absent or resource.replace?
        return true
      end

      if should == :present
        !(currentvalue.nil? or currentvalue == :absent)
      else
        super(currentvalue)
      end
    end

    def retrieve
      stat = @resource.stat
      if stat
        stat.ftype.intern
      elsif should == :false
        :false
      else
        :absent
      end
    end

    def sync
      @resource.remove_existing(should)
      if should == :absent
        return :file_removed
      end

      super
    end
  end
end
