<?php

namespace pictpostpersonal;

/**
 * URLパターンに応じてアクションを振り分けるルータ (PHP 7.1対応版)
 */
class Router
{
    /** @var RequestInterface */
    private $request;

    /** @var ResponseEmitterInterface */
    private $response_emitter;

    /** @var Context|null 依存性注入コンテナ */
    private $context = null;

    /**
     * @var array メソッド別ルート配列
     *     例: $routes["get"]["/hoge/{id}"] = callable|string|array
     */
    private $routes = [];

    /** @var mixed 404エラー時のアクション (callable|string|array) */
    private $error404_func = null;

    /** @var mixed デフォルトルート (callable|string|array) */
    private $default_route_func = null;

    /**
     * @var string publicディレクトリのパス
     *     例: "/path/to/public"
     */
    private $public_root = '';

    /**
     * コンストラクタ
     *
     * @param RequestInterface $req
     * @param string  $publicRoot publicディレクトリのパス
     * @param ResponseEmitterInterface $responseEmitter
     * @param Context|null $context 依存性注入コンテナ
     */
    public function __construct($req, $public_root = '', $response_emitter = null, $context = null)
    {
        if ($response_emitter === null) {
            $response_emitter = new ResponseEmitter();
        }
        $this->response_emitter = $response_emitter;
        $this->public_root = $public_root;
        $this->request = $req;
        $this->context = $context;
    }

    /**
     * デフォルトルート登録
     *
     * @param $action (callable|string|array)
     */
    public function defaultRoute($action)
    {
        $this->default_route_func = $action;
    }

    /**
     * 404時のアクション登録
     *
     * @param $action
     */
    public function error404($action)
    {
        $this->error404_func = $action;
    }

    /**
     * 全HTTPメソッド共通のルート登録
     *
     * @param string $command
     * @param mixed  $action
     */
    public function any($command, $action)
    {
        foreach (['get', 'post', 'delete', 'put', 'patch'] as $m) {
            if (!isset($this->routes[$m])) {
                $this->routes[$m] = [];
            }
            $this->routes[$m][$command] = $action;
        }
    }

    public function get($command, $action)
    {
        $this->routes['get'][$command] = $action;
    }
    public function post($command, $action)
    {
        $this->routes['post'][$command] = $action;
    }
    public function delete($command, $action)
    {
        $this->routes['delete'][$command] = $action;
    }
    public function put($command, $action)
    {
        $this->routes['put'][$command] = $action;
    }
    public function patch($command, $action)
    {
        $this->routes['patch'][$command] = $action;
    }

    /**
     * ルータのメイン処理
     */
    public function run()
    {
        $method = $this->request->method;
        // 定義がないメソッドなら404扱い 
        if (!isset($this->routes[$method])) {
            $this->processNoRouteFound();
            return;
        }

        // URLパターンにマッチするアクションを探す
        $action = $this->matchAction($method);
        if (!$action) {
            // 静的ファイルがあれば返す、なければデフォルト or 404
            if ($this->tryServeStaticFile($this->request->command)) {
                return;
            }
            if (!empty($this->default_route_func)) {
                $action = $this->default_route_func;
            } else {
                $this->processNoRouteFound();
                return;
            }
        }

        // アクション実行
        $this->executeAction($action);
    }

    /**
     * メソッド配下のルート一覧を走査し、URLパターンに合致するアクションを返す
     *
     * @param string $method
     * @return mixed (callable|string|array|null)
     */
    private function matchAction($method)
    {

        foreach ($this->routes[$method] as $command => $obj) {
            if ($this->request->urlPatternMatch($command)) {
                // pathArgs["action"] がセットされていれば、クラス:actionName 拡張
                if (is_string($obj) && isset($this->request->path_args['action'])) {
                    return $obj . ':' . $this->request->path_args['action'];
                }
                return $obj;
            }
        }

        // "/{command}" で登録されている場合のfallback
        $fallback_key = '/' . $this->request->command;
        if (isset($this->routes[$method][$fallback_key])) {
            $fallback = $this->routes[$method][$fallback_key];
            if (is_string($fallback) && isset($this->request->path_args['action'])) {
                return $fallback . ':' . $this->request->path_args['action'];
            }
            return $fallback;
        }
        return null;
    }

    /**
     * 該当ルートがない場合の処理(404など)
     */
    private function processNoRouteFound()
    {
        $this->response_emitter->setStatus(404);
        $this->response_emitter->setHeader('HTTP/1.1 404 Not Found');
        if (!empty($this->error404_func)) {
            $this->executeAction($this->error404_func);
        } else {
            $this->response_emitter->setHeader('Content-Type: text/html; charset=UTF-8');
            $this->response_emitter->output('404 FILE NOT FOUND');
        }
    }

    /**
     * publicディレクトリの静的ファイルを返せるか試みる 
     *
     * @param string $command
     * @return bool 成功したらtrue
     */
    private function tryServeStaticFile($command)
    {
        // publicRootが空なら静的ファイルは無視
        if ($this->public_root === '') {
            return false;
        }

        // コマンドが空なら無視
        if (trim($command) === '') {
            return false;
        }

        // コマンドにディレクトリトラバーサルや不正な文字が含まれていないかチェック
        if (strpos($command, '..') !== false || strpos($command, '\\') !== false || strpos($command, '\0') !== false) {
            return false;
        }

        // ファイル名に許可しない文字列が含まれていないかチェック（nullバイト, バックスラッシュ, 制御文字など）
        if (!preg_match('/^[a-zA-Z0-9_\-\/\.\(\)]+$/', $command)) {
            return false;
        }

        $public_root = $this->public_root;
        $target_path = realpath($public_root . '/' . $command);

        // realpath が失敗した場合、またはファイルが存在しない場合
        // または、publicRoot外のパスの場合は false を返す
        if (!$target_path || !is_file($target_path)) {
            return false;
        }
        if (substr($target_path, 0, strlen($public_root)) !== $public_root) {
            return false;
        }




        // ファイルが存在する場合はmimetypeを取得して出力
        $mime_type = function_exists('mime_content_type')
            ? mime_content_type($target_path)
            : 'application/octet-stream';

        $this->response_emitter->setStatus(200);
        $this->response_emitter->outputFile($target_path, $mime_type);


        return true;
    }

    /**
     * アクションを実行する
     *
     * @param $action (callable|string|array)
     * @return void
     */
    private function executeAction($action)
    {
        // 配列ならミドルウェア等で順次実行
        if (is_array($action)) {
            foreach ($action as $item) {
                $ret = $this->executeActionAndCheck($item);
                if (!$ret) {
                    return;
                }
            }
            return;
        }

        // 文字列 "ClassName:methodName" 形式
        if (is_string($action)) {
            $parts = explode(':', $action, 2);
            $class_name  = isset($parts[0]) ? $parts[0] : '';
            $method_name = isset($parts[1]) ? $parts[1] : 'show';

            $fq_class_name = "pictpostpersonal\\controller\\{$class_name}";
            if (!class_exists($fq_class_name)) {
                $this->logOrEcho("Class $fq_class_name not found.");
                return;
            }
            $instance = new $fq_class_name($this->context);
            if (!method_exists($instance, $method_name)) {
                $this->logOrEcho("Method $method_name not found in class $fq_class_name");
                return;
            }
            $result = $instance->$method_name($this->request);
            $this->handleResult($result);
            return;
        }

        // callable (クロージャや関数)
        if (is_callable($action)) {
            $result = call_user_func($action, $this->request);
            $this->handleResult($result);
            return;
        }
    }

    /**
     * executeAction の再帰呼び出し用
     *
     * @param $action
     * @return bool
     */
    private function executeActionAndCheck($action)
    {
        // ミドルウェア配列
        if (is_array($action)) {
            foreach ($action as $item) {
                $ret = $this->executeActionAndCheck($item);
                if (!$ret) {
                    return false;
                }
            }
            return true;
        }

        // 文字列 "Class:Method"
        if (is_string($action)) {
            $parts = explode(':', $action, 2);
            $class_name  = isset($parts[0]) ? $parts[0] : '';
            $method_name = isset($parts[1]) ? $parts[1] : 'show';

            $fq_class_name = "pictpostpersonal\\controller\\{$class_name}";
            if (!class_exists($fq_class_name)) {
                $this->logOrEcho("Class $fq_class_name not found.");
                return false;
            }
            $instance = new $fq_class_name($this->context);
            if (!method_exists($instance, $method_name)) {
                $this->logOrEcho("Method $method_name not found in class $fq_class_name");
                return false;
            }
            $result = $instance->$method_name($this->request);
            return $this->handleResult($result);
        }

        // callable
        if (is_callable($action)) {
            $result = call_user_func($action, $this->request);
            return $this->handleResult($result);
        }

        // 想定外の型
        return false;
    }

    /**
     * 実行結果を判定して出力やリダイレクトなどを行う
     *
     * @param $result
     * @return bool 処理継続フラグ
     */
    private function handleResult($result)
    {
        if ($result === null) {
            // 何も返さない -> 画面出力済みとみなし継続
            return true;
        }

        if (!$result instanceof Result) {
            $this->logOrEcho('The action did not return a valid Result object.');
            return false;
        }

        // updated_request があれば差し替える
        if (!empty($result->updated_request)) {
            $this->request = $result->updated_request;
        }

        // redirect_action があれば再帰的に実行
        if (!empty($result->redirect_action)) {
            return $this->executeActionAndCheck($result->redirect_action);
        }

        // リダイレクトURLがあれば即リダイレクト
        if (!empty($result->redirect)) {
            Result::redirect($result->redirect, $result->status);
            return false;
        }

        // status=0 -> 出力なしで継続
        if ($result->status === 0) {
            return $result->next;
        }

        // 200: 通常出力
        if ($result->status === 200) {
            if (!empty($result->mime_type)) {
                $this->response_emitter->setHeader("Content-Type: {$result->mime_type}");
            }
            $this->response_emitter->output($result->output);
            return $result->next;
        }

        // 404
        if ($result->status === 404) {
            if (!empty($result->output)) {
                $this->response_emitter->setStatus(404);
                $this->response_emitter->output($result->output);
                return false;
            }
            // エラー404ハンドラ呼び出し
            if (!empty($this->error404_func)) {
                http_response_code(404);
                $res = call_user_func($this->error404_func, $this->request);
                if ($res instanceof Result) {
                    return $this->handleResult($res);
                }
                return false;
            }
            $this->response_emitter->setStatus(404);
            $this->response_emitter->output('404 FILE NOT FOUND');
            return false;
        }

        // その他(500等)
        $this->response_emitter->setStatus($result->status);
        $this->response_emitter->output($result->output);
        return false;
    }

    /**
     * ログに書き込み、またはecho
     */
    private function logOrEcho($msg)
    {
        $this->context->getLog()->log($msg);
    }
}
