# Copyright VyOS maintainers and contributors <maintainers@vyos.io>
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this library.  If not, see <http://www.gnu.org/licenses/>.

# T5160: Firewall re-writing

#  cli changes from:
#  set firewall name <name> ...
#  set firewall ipv6-name <name> ...
#  To
#  set firewall ipv4 name <name>
#  set firewall ipv6 name <name>

## Also from 'firewall interface' removed.
## in and out:
    # set firewall interface <iface> [in|out] [name | ipv6-name] <name>
    # To
    # set firewall [ipv4 | ipv6] forward filter rule <5,10,15,...> [inbound-interface | outboubd-interface] interface-name <iface>
    # set firewall [ipv4 | ipv6] forward filter rule <5,10,15,...> action jump
    # set firewall [ipv4 | ipv6] forward filter rule <5,10,15,...> jump-target <name>
## local:
    # set firewall interface <iface> local [name | ipv6-name] <name>
    # To
    # set firewall [ipv4 | ipv6] input filter rule <5,10,15,...> inbound-interface interface-name <iface>
    # set firewall [ipv4 | ipv6] input filter rule <5,10,15,...> action jump
    # set firewall [ipv4 | ipv6] input filter rule <5,10,15,...> jump-target <name>

# T8281: Normalize firewall rule network prefixes for source and destination address
#
# Fixes invalid/non-canonical prefixes like:
#   10.10.10.1/30   -> 10.10.10.0/30
#   2001:db8::1/64  -> 2001:db8::/64
#
# Only "source address" and "destination address" are rewritten.

from ipaddress import ip_network
from vyos.configtree import ConfigTree

base = ['firewall']


def normalize_address(config: ConfigTree, path: list[str]):
    if not config.exists(path):
        return

    def normalize(value: str) -> str | None:
        if '/' not in value:
            return None

        is_except = value.startswith('!')
        if is_except:
            value = value.lstrip('!')

        try:
            new_value = str(ip_network(value, strict=False))
        except ValueError:
            return None

        return f'!{new_value}' if is_except else new_value

    value = config.return_value(path)
    if value:
        normalized = normalize(value)
        if normalized and normalized != value:
            config.set(path, value=normalized)


def migrate_ruleset(config: ConfigTree, ruleset_base: list[str]):
    if not config.exists(ruleset_base + ['rule']):
        return

    for rule_id in config.list_nodes(ruleset_base + ['rule']):
        rule_base = ruleset_base + ['rule', rule_id]

        for direction in ['source', 'destination']:
            normalize_address(config, rule_base + [direction, 'address'])


def migrate(config: ConfigTree) -> None:
    if not config.exists(base):
        # Nothing to do
        return

    ### Migration of state policies
    if config.exists(base + ['state-policy']):
        for state in config.list_nodes(base + ['state-policy']):
            action = config.return_value(base + ['state-policy', state, 'action'])
            config.set(base + ['global-options', 'state-policy', state, 'action'], value=action)
            if config.exists(base + ['state-policy', state, 'log']):
                config.set(base + ['global-options', 'state-policy', state, 'log'], value='enable')
        config.delete(base + ['state-policy'])

    ## migration of global options:
    for option in ['all-ping', 'broadcast-ping', 'config-trap', 'ip-src-route', 'ipv6-receive-redirects', 'ipv6-src-route', 'log-martians',
                    'receive-redirects', 'resolver-cache', 'resolver-internal', 'send-redirects', 'source-validation', 'syn-cookies', 'twa-hazards-protection']:
        if config.exists(base + [option]):
            if option != 'config-trap':
                val = config.return_value(base + [option])
                config.set(base + ['global-options', option], value=val)
            config.delete(base + [option])

    ### Migration of firewall name and ipv6-name
    ### Also migrate legacy 'accept' behaviour
    if config.exists(base + ['name']):
        config.set(['firewall', 'ipv4', 'name'])
        config.set_tag(['firewall', 'ipv4', 'name'])

        for ipv4name in config.list_nodes(base + ['name']):
            config.copy(base + ['name', ipv4name], base + ['ipv4', 'name', ipv4name])

            if config.exists(base + ['ipv4', 'name', ipv4name, 'default-action']):
                action = config.return_value(base + ['ipv4', 'name', ipv4name, 'default-action'])

                if action == 'accept':
                    config.set(base + ['ipv4', 'name', ipv4name, 'default-action'], value='return')

            if config.exists(base + ['ipv4', 'name', ipv4name, 'rule']):
                for rule_id in config.list_nodes(base + ['ipv4', 'name', ipv4name, 'rule']):
                    action = config.return_value(base + ['ipv4', 'name', ipv4name, 'rule', rule_id, 'action'])

                    if action == 'accept':
                        config.set(base + ['ipv4', 'name', ipv4name, 'rule', rule_id, 'action'], value='return')

        config.delete(base + ['name'])

    if config.exists(base + ['ipv6-name']):
        config.set(['firewall', 'ipv6', 'name'])
        config.set_tag(['firewall', 'ipv6', 'name'])

        for ipv6name in config.list_nodes(base + ['ipv6-name']):
            config.copy(base + ['ipv6-name', ipv6name], base + ['ipv6', 'name', ipv6name])

            if config.exists(base + ['ipv6', 'name', ipv6name, 'default-action']):
                action = config.return_value(base + ['ipv6', 'name', ipv6name, 'default-action'])

                if action == 'accept':
                    config.set(base + ['ipv6', 'name', ipv6name, 'default-action'], value='return')

            if config.exists(base + ['ipv6', 'name', ipv6name, 'rule']):
                for rule_id in config.list_nodes(base + ['ipv6', 'name', ipv6name, 'rule']):
                    action = config.return_value(base + ['ipv6', 'name', ipv6name, 'rule', rule_id, 'action'])

                    if action == 'accept':
                        config.set(base + ['ipv6', 'name', ipv6name, 'rule', rule_id, 'action'], value='return')

        config.delete(base + ['ipv6-name'])

    ### Migration of firewall interface
    if config.exists(base + ['interface']):
        fwd_ipv4_rule = 5
        inp_ipv4_rule = 5
        fwd_ipv6_rule = 5
        inp_ipv6_rule = 5
        for direction in ['in', 'out', 'local']:
            for iface in config.list_nodes(base + ['interface']):
                if config.exists(base + ['interface', iface, direction]):
                    if config.exists(base + ['interface', iface, direction, 'name']):
                        target = config.return_value(base + ['interface', iface, direction, 'name'])
                        if direction == 'in':
                            # Add default-action== accept for compatibility reasons:
                            config.set(base + ['ipv4', 'forward', 'filter', 'default-action'], value='accept')
                            new_base = base + ['ipv4', 'forward', 'filter', 'rule']
                            config.set(new_base)
                            config.set_tag(new_base)
                            config.set(new_base + [fwd_ipv4_rule, 'inbound-interface', 'interface-name'], value=iface)
                            config.set(new_base + [fwd_ipv4_rule, 'action'], value='jump')
                            config.set(new_base + [fwd_ipv4_rule, 'jump-target'], value=target)
                            fwd_ipv4_rule = fwd_ipv4_rule + 5
                        elif direction == 'out':
                            # Add default-action== accept for compatibility reasons:
                            config.set(base + ['ipv4', 'forward', 'filter', 'default-action'], value='accept')
                            new_base = base + ['ipv4', 'forward', 'filter', 'rule']
                            config.set(new_base)
                            config.set_tag(new_base)
                            config.set(new_base + [fwd_ipv4_rule, 'outbound-interface', 'interface-name'], value=iface)
                            config.set(new_base + [fwd_ipv4_rule, 'action'], value='jump')
                            config.set(new_base + [fwd_ipv4_rule, 'jump-target'], value=target)
                            fwd_ipv4_rule = fwd_ipv4_rule + 5
                        else:
                            # Add default-action== accept for compatibility reasons:
                            config.set(base + ['ipv4', 'input', 'filter', 'default-action'], value='accept')
                            new_base = base + ['ipv4', 'input', 'filter', 'rule']
                            config.set(new_base)
                            config.set_tag(new_base)
                            config.set(new_base + [inp_ipv4_rule, 'inbound-interface', 'interface-name'], value=iface)
                            config.set(new_base + [inp_ipv4_rule, 'action'], value='jump')
                            config.set(new_base + [inp_ipv4_rule, 'jump-target'], value=target)
                            inp_ipv4_rule = inp_ipv4_rule + 5

                    if config.exists(base + ['interface', iface, direction, 'ipv6-name']):
                        target = config.return_value(base + ['interface', iface, direction, 'ipv6-name'])
                        if direction == 'in':
                            # Add default-action== accept for compatibility reasons:
                            config.set(base + ['ipv6', 'forward', 'filter', 'default-action'], value='accept')
                            new_base = base + ['ipv6', 'forward', 'filter', 'rule']
                            config.set(new_base)
                            config.set_tag(new_base)
                            config.set(new_base + [fwd_ipv6_rule, 'inbound-interface', 'interface-name'], value=iface)
                            config.set(new_base + [fwd_ipv6_rule, 'action'], value='jump')
                            config.set(new_base + [fwd_ipv6_rule, 'jump-target'], value=target)
                            fwd_ipv6_rule = fwd_ipv6_rule + 5
                        elif direction == 'out':
                            # Add default-action== accept for compatibility reasons:
                            config.set(base + ['ipv6', 'forward', 'filter', 'default-action'], value='accept')
                            new_base = base + ['ipv6', 'forward', 'filter', 'rule']
                            config.set(new_base)
                            config.set_tag(new_base)
                            config.set(new_base + [fwd_ipv6_rule, 'outbound-interface', 'interface-name'], value=iface)
                            config.set(new_base + [fwd_ipv6_rule, 'action'], value='jump')
                            config.set(new_base + [fwd_ipv6_rule, 'jump-target'], value=target)
                            fwd_ipv6_rule = fwd_ipv6_rule + 5
                        else:
                            new_base = base + ['ipv6', 'input', 'filter', 'rule']
                            # Add default-action== accept for compatibility reasons:
                            config.set(base + ['ipv6', 'input', 'filter', 'default-action'], value='accept')
                            config.set(new_base)
                            config.set_tag(new_base)
                            config.set(new_base + [inp_ipv6_rule, 'inbound-interface', 'interface-name'], value=iface)
                            config.set(new_base + [inp_ipv6_rule, 'action'], value='jump')
                            config.set(new_base + [inp_ipv6_rule, 'jump-target'], value=target)
                            inp_ipv6_rule = inp_ipv6_rule + 5

        config.delete(base + ['interface'])

    ### Normalize/fix firewall rule network prefixes for source and destination address (T8281)
    for family in ['ipv4', 'ipv6', 'bridge']:
        # 1) Named rulesets:
        #    set firewall ipv4|ipv6|bridge name <ruleset> rule <id> ...
        ruleset_base = base + [family, 'name']
        if config.exists(ruleset_base):
            for ruleset in config.list_nodes(ruleset_base):
                migrate_ruleset(config, ruleset_base + [ruleset])

        # 2) Hook-based rulesets:
        #    set firewall ipv4|ipv6|bridge input|output|forward filter|raw rule <id> ...
        #    set firewall ipv4|ipv6 prerouting raw rule <id> ...
        hook_rulesets = {
            'input': ['filter', 'raw'],
            'output': ['filter', 'raw'],
            'forward': ['filter', 'raw'],
            'prerouting': ['raw'],
        }
        for hook, rulesets in hook_rulesets.items():
            for ruleset in rulesets:
                migrate_ruleset(config, base + [family, hook, ruleset])
