# Copyright 2010-2011 OpenStack Foundation
# Copyright (c) 2013 Hewlett-Packard Development Company, L.P.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.

from __future__ import annotations

from collections.abc import Iterator
import logging
import re

import pcre2

LOG = logging.getLogger()


class Rule:
    "Base class for rules."

    linenum: int
    code: str
    pattern: str
    target: str | None

    def __init__(self, linenum: int, *params: str | None) -> None:
        self.linenum = linenum
        self._params = params
        if len(params) == 4:
            # redirect code pattern target
            assert params[1] is not None
            assert params[2] is not None
            self.code = params[1]
            self.pattern = params[2]
            self.target = params[3]
        elif len(params) == 3:
            assert params[1] is not None
            assert params[2] is not None
            if params[1] == '410':
                # The page has been deleted and is not coming back.
                self.code = params[1]
                self.pattern = params[2]
                self.target = None
            else:
                # redirect pattern target
                # (code is implied)
                self.code = '301'
                self.pattern = params[1]
                self.target = params[2]
        else:
            raise ValueError(f'Could not understand rule {params}')

    def __str__(self) -> str:
        return '[{}] {}'.format(
            self.linenum,
            ' '.join(p for p in self._params if p),
        )

    def match(self, path: str) -> tuple[str, str | None] | None:
        raise NotImplementedError('Base class does not implement match()')


class Redirect(Rule):
    "A Redirect rule."

    def match(self, path: str) -> tuple[str, str | None] | None:
        if path == self.pattern:
            return (self.code, self.target)
        return None


class RedirectMatch(Rule):
    "A RedirectMatch rule with a regular expression."

    _group_subst = re.compile(r'(?<!\\)\$([0-9])')

    def __init__(self, linenum: int, *params: str | None) -> None:
        super().__init__(linenum, *params)
        self.regex = pcre2.compile(self.pattern)
        if self.target:
            self.target_repl: str | None = self._get_target_repl()
        else:
            self.target_repl = None

    def _get_target_repl(self) -> str | None:
        return self.target

    def match(self, path: str) -> tuple[str, str | None] | None:
        m = self.regex.search(path)
        if m:
            if self.target_repl:
                return (self.code, self.regex.sub(self.target_repl, path))
            else:
                # A rule that doesn't have a response target, like 410.
                return (self.code, self.target_repl)
        return None


class RuleSet:
    "An ordered collection of rules."

    _factories: dict[str, type[Rule]] = {
        'redirect': Redirect,
        'redirectmatch': RedirectMatch,
    }

    def __init__(self) -> None:
        self._rules: list[Rule] = []
        self._by_num: dict[int, Rule] = {}

    def add(self, linenum: int, *params: str | None) -> None:
        assert params[0] is not None
        rule_type = params[0].lower()
        rule = self._factories[rule_type](linenum, *params)
        self._rules.append(rule)
        self._by_num[linenum] = rule

    def __getitem__(self, index: int) -> Rule:
        return self._by_num[index]

    def __iter__(self) -> Iterator[Rule]:
        return iter(self._rules)

    @property
    def all_ids(self) -> list[int]:
        return list(self._by_num.keys())

    def match(self, path: str) -> tuple[int, str, str | None] | None:
        for rule in self:
            try:
                m = rule.match(path)
            except Exception as e:
                LOG.warning(
                    'Failed to evaluate %s against %s: %s', rule, path, e
                )
            else:
                if m is not None:
                    LOG.debug(
                        'Matched "%s" for path "%s" producing %s',
                        rule,
                        path,
                        m,
                    )
                    return (rule.linenum,) + m
        return None
