<?php
declare(strict_types=1);

namespace App\Http;

use App\Community\CommentService;
use App\Community\FollowService;
use App\Community\PostingPolicyService;
use App\Notifications\NotificationService;
use App\Questions\AnswerService;
use App\Questions\QuestionService;
use App\Questions\VoteService;
use App\Security\RateLimitService;
use App\Settings\SettingsRepository;
use App\Support\Translator;
use App\Support\FileLogger;
use InvalidArgumentException;
use PDO;
use Throwable;

final class ApiMutationController
{
    /** @param list<string> $scopes */
    public function __construct(
        private readonly PDO $pdo,
        private readonly ?array $actor,
        private readonly array $scopes,
        private readonly SettingsRepository $settings,
        private readonly Translator $translator,
        private readonly string $baseUrl = '',
    ) {}

    public function createQuestion(array $input): JsonResponse
    {
        $policy = PostingPolicyService::fromSettings($this->settings);
        return $this->run('questions:write','question.create',max(1,$policy->int('question_rate_hour')),3600,function(int $userId)use($input,$policy):JsonResponse{
            $tagIds=is_array($input['tag_ids']??null)?array_values(array_unique(array_filter(array_map('intval',$input['tag_ids']),static fn(int $id):bool=>$id>0))):[];
            $title=(string)($input['title']??'');$body=(string)($input['body']??'');
            $policy->validateQuestion($title,$body,count($tagIds));
            $id=(new QuestionService($this->pdo))->create($userId,$title,$body,($cid=(int)($input['category_id']??0))>0?$cid:null,(string)($input['content_locale']??($this->actor['preferred_locale']??'ar')),!empty($input['is_anonymous']),$tagIds);
            try{(new NotificationService($this->pdo,$this->settings))->notifyAdmins('new_question',$this->translator->get('notifications.new_question_title'),(string)($input['title']??''),$this->absolute(quaeriva_admin_path(),['page'=>'questions']));}catch(Throwable $throwable){(new FileLogger())->error('API question notification failed.',$throwable);}
            return JsonResponse::success(['id'=>$id,'status'=>'pending'],[],201);
        });
    }

    public function createAnswer(int $questionId,array $input): JsonResponse
    {
        $policy = PostingPolicyService::fromSettings($this->settings);
        return $this->run('answers:write','answer.create',max(1,$policy->int('answer_rate_hour')),3600,function(int $userId)use($questionId,$input,$policy):JsonResponse{
            $body=(string)($input['body']??'');$policy->validateAnswer($body);
            $id=(new AnswerService($this->pdo))->create($userId,$questionId,$body);
            $this->notifyQuestionParticipants($questionId,'new_answer',$userId,$id);
            return JsonResponse::success(['id'=>$id,'question_id'=>$questionId],[],201);
        });
    }

    public function createComment(string $type,int $targetId,array $input): JsonResponse
    {
        $policy = PostingPolicyService::fromSettings($this->settings);
        return $this->run('comments:write','comment.create',max(1,$policy->int('comment_rate_hour')),3600,function(int $userId)use($type,$targetId,$input,$policy):JsonResponse{
            $type=strtolower(trim($type));
            if(!in_array($type,['answer','topic'],true))throw new InvalidArgumentException('Public comments are only supported on answers and topics.');
            $body=(string)($input['body']??'');$policy->validateComment($body);
            $id=(new CommentService($this->pdo))->create($userId,$type,$targetId,$body,$policy->int('duplicate_content_minutes'));
            if ($type === 'answer') {
                $questionId=$this->questionIdForTarget('answer',$targetId);$this->notifyQuestionParticipants($questionId,'new_comment',$userId);
            }
            return JsonResponse::success(['id'=>$id,'target_type'=>$type,'target_id'=>$targetId],[],201);
        });
    }

    public function castVote(array $input): JsonResponse
    {
        return $this->run('votes:write','vote.cast',120,3600,function(int $userId)use($input):JsonResponse{
            $type=(string)($input['target_type']??$input['votable_type']??'question');$id=(int)($input['target_id']??$input['votable_id']??0);$value=(int)($input['value']??0);
            (new VoteService($this->pdo))->cast($userId,$type,$id,$value);return JsonResponse::success(['target_type'=>$type,'target_id'=>$id,'value'=>$value]);
        });
    }

    public function toggleFollow(array $input): JsonResponse
    {
        return $this->run('follows:write','follow.toggle',120,3600,function(int $userId)use($input):JsonResponse{
            $type=(string)($input['target_type']??'question');$id=(int)($input['target_id']??0);$following=(new FollowService($this->pdo))->toggle($userId,$type,$id);return JsonResponse::success(['target_type'=>$type,'target_id'=>$id,'following'=>$following]);
        });
    }

    public function acceptAnswer(int $answerId): JsonResponse
    {
        return $this->run('answers:accept','answer.accept',30,3600,function(int $userId)use($answerId):JsonResponse{
            $lookup=$this->pdo->prepare("SELECT a.user_id,a.question_id,q.title,q.slug FROM answers a INNER JOIN questions q ON q.id=a.question_id WHERE a.id=:id AND a.status='published' LIMIT 1");
            $lookup->execute(['id'=>$answerId]);$answer=$lookup->fetch(PDO::FETCH_ASSOC);
            if(!is_array($answer))throw new InvalidArgumentException('Answer not found.');
            (new AnswerService($this->pdo))->accept($userId,$answerId);
            $ownerId=(int)($answer['user_id']??0);
            if($ownerId>0&&$ownerId!==$userId){
                try{(new NotificationService($this->pdo,$this->settings))->notifyUsers([$ownerId],'answer_accepted',$this->translator->get('notifications.answer_accepted_title'),(string)($answer['title']??''),$this->absolute('q/'.rawurlencode((string)($answer['slug']??''))).'#answer-'.$answerId);}catch(Throwable $throwable){(new FileLogger())->error('API acceptance notification failed.',$throwable);}
            }
            return JsonResponse::success(['answer_id'=>$answerId,'accepted'=>true]);
        });
    }

    public function createReport(array $input): JsonResponse
    {
        return $this->run('reports:write','report.create',20,3600,function(int $userId)use($input):JsonResponse{
            $type=strtolower(trim((string)($input['reportable_type']??$input['target_type']??'question')));
            $targetId=(int)($input['reportable_id']??$input['target_id']??0);
            $reason=strtolower(trim((string)($input['reason']??'')));
            $notes=trim((string)($input['notes']??''));
            if((!in_array($type,['question','answer','comment'],true) && $type !== 'topic')||$targetId<=0)throw new InvalidArgumentException('Invalid report target.');
            if(!in_array($reason,['spam','abuse','duplicate','misleading','other'],true))throw new InvalidArgumentException('Invalid report reason.');
            if((function_exists('mb_strlen')?mb_strlen($notes,'UTF-8'):strlen($notes))>1000)throw new InvalidArgumentException('Report notes are too long.');
            if($type==='question'){
                $check=$this->pdo->prepare("SELECT q.id,q.title,q.slug FROM questions q WHERE q.id=:id AND q.status='published' LIMIT 1");
            }elseif($type==='answer'){
                $check=$this->pdo->prepare("SELECT a.id,q.title,q.slug FROM answers a INNER JOIN questions q ON q.id=a.question_id WHERE a.id=:id AND a.status='published' AND q.status='published' LIMIT 1");
            }elseif($type==='topic'){
                $check=$this->pdo->prepare("SELECT id,title,slug FROM topics WHERE id=:id AND status='published' LIMIT 1");
            }else{
                $check=$this->pdo->prepare(
                    "SELECT c.id,COALESCE(qd.title,qa.title,tt.title) AS title,COALESCE(qd.slug,qa.slug,tt.slug) AS slug
                     FROM comments c
                     LEFT JOIN questions qd ON c.commentable_type='question' AND qd.id=c.commentable_id
                     LEFT JOIN answers a ON c.commentable_type='answer' AND a.id=c.commentable_id
                     LEFT JOIN questions qa ON qa.id=a.question_id
                     LEFT JOIN topics tt ON c.commentable_type='topic' AND tt.id=c.commentable_id
                     WHERE c.id=:id AND c.status='published' AND COALESCE(qd.status,qa.status,tt.status)='published' LIMIT 1"
                );
            }
            $check->execute(['id'=>$targetId]);$target=$check->fetch(PDO::FETCH_ASSOC);if(!is_array($target))throw new InvalidArgumentException('Report target not found.');
            $existing=$this->pdo->prepare("SELECT id FROM reports WHERE reportable_type=:type AND reportable_id=:target_id AND user_id=:user_id AND status IN ('open','reviewing') LIMIT 1");
            $existing->execute(['type'=>$type,'target_id'=>$targetId,'user_id'=>$userId]);
            $existingId=$existing->fetchColumn();
            if($existingId!==false)return JsonResponse::success(['id'=>(int)$existingId,'status'=>'open','duplicate'=>true]);
            $insert=$this->pdo->prepare("INSERT INTO reports (reportable_type,reportable_id,user_id,reason,status,notes,created_at,updated_at) VALUES (:type,:target_id,:user_id,:reason,'open',:notes,NOW(),NOW())");
            $insert->execute(['type'=>$type,'target_id'=>$targetId,'user_id'=>$userId,'reason'=>$reason,'notes'=>$notes===''?null:$notes]);
            $id=(int)$this->pdo->lastInsertId();
            try{(new NotificationService($this->pdo,$this->settings))->notifyAdmins('new_report',$this->translator->get('notifications.new_report_title'),(string)($target['title']??($type.' #'.$targetId)),$this->absolute(quaeriva_admin_path(),['page'=>'reports','view_report'=>$id]));}catch(Throwable){}
            return JsonResponse::success(['id'=>$id,'status'=>'open'],[],201);
        });
    }

    public function notifications(): JsonResponse
    {
        $auth=$this->authorize('notifications:read');if($auth instanceof JsonResponse)return $auth;
        return JsonResponse::success((new NotificationService($this->pdo,$this->settings))->inbox((int)$auth['id'],100));
    }

    public function me(): JsonResponse
    {
        $auth=$this->authorize('profile:read');if($auth instanceof JsonResponse)return $auth;
        return JsonResponse::success(['id'=>(int)$auth['id'],'name'=>(string)($auth['name']??''),'username'=>$auth['username']??null,'email'=>(string)($auth['email']??''),'role'=>(string)($auth['role']??'member'),'points'=>(int)($auth['points']??0),'preferred_locale'=>(string)($auth['preferred_locale']??'ar')]);
    }

    private function run(string $scope,string $action,int $limit,int $seconds,callable $callback): JsonResponse
    {
        $auth=$this->authorize($scope);if($auth instanceof JsonResponse)return $auth;$userId=(int)$auth['id'];
        try{if(!(new RateLimitService($this->pdo))->consume('api-user:'.$userId,$action,$limit,$seconds))return JsonResponse::failure(429,'rate_limited',$this->message('rate_limited'));return $callback($userId);}catch(\App\Support\TranslatableException $e){return JsonResponse::failure(422,'invalid_request',$e->translate($this->translator));}catch(InvalidArgumentException){return JsonResponse::failure(422,'invalid_request',$this->message('mutation_failed'));}catch(Throwable){return JsonResponse::failure(422,'mutation_failed',$this->message('mutation_failed'));}
    }

    /** @return array<string,mixed>|JsonResponse */
    private function authorize(string $scope): array|JsonResponse
    {
        if(!is_array($this->actor)||(int)($this->actor['id']??0)<=0)return JsonResponse::failure(401,'unauthorized',$this->message('unauthorized'));
        if(!in_array('*',$this->scopes,true)&&!in_array($scope,$this->scopes,true))return JsonResponse::failure(403,'forbidden_scope',$this->message('forbidden_scope'),['required_scope'=>$scope]);
        return $this->actor;
    }

    private function questionIdForTarget(string $type,int $targetId): int
    {
        if($type==='question')return $targetId;$s=$this->pdo->prepare("SELECT question_id FROM answers WHERE id=:id AND status='published' LIMIT 1");$s->execute(['id'=>$targetId]);return (int)$s->fetchColumn();
    }

    private function notifyQuestionParticipants(int $questionId,string $event,int $exclude,?int $answerId=null): void
    {
        try{
            if(!(bool)$this->settings->get('notifications.'.$event,true))return;
            $q=$this->pdo->prepare("SELECT user_id,title,slug FROM questions WHERE id=:id AND status='published' LIMIT 1");$q->execute(['id'=>$questionId]);$question=$q->fetch(PDO::FETCH_ASSOC);if(!is_array($question))return;
            $ids=[(int)$question['user_id']];$f=$this->pdo->prepare('SELECT user_id FROM question_follows WHERE question_id=:id');$f->execute(['id'=>$questionId]);$ids=array_merge($ids,array_map('intval',$f->fetchAll(PDO::FETCH_COLUMN)));
            $query=[];$fragment='';
            if($answerId!==null&&$answerId>0){$query['answers_page']=$this->answerPage($questionId,$answerId);$fragment='#answer-'.$answerId;}
            (new NotificationService($this->pdo,$this->settings))->notifyUsers($ids,$event,$this->translator->get('notifications.'.$event.'_title'),(string)$question['title'],$this->absolute('q/'.rawurlencode((string)$question['slug']),$query).$fragment,[$exclude]);
        }catch(Throwable $throwable){(new FileLogger())->error('API participant notification failed.',$throwable,['question_id'=>$questionId]);}
    }

    private function absolute(string $path,array $query=[]): string
    {
        $url=rtrim($this->baseUrl,'/').'/'.ltrim($path,'/');if($query!==[])$url.='?'.http_build_query($query,'','&',PHP_QUERY_RFC3986);return $url;
    }

    private function answerPage(int $questionId,int $answerId,int $perPage=50): int
    {
        $target=$this->pdo->prepare("SELECT is_best,created_at,id FROM answers WHERE id=:id AND question_id=:question_id AND status='published' LIMIT 1");$target->execute(['id'=>$answerId,'question_id'=>$questionId]);$answer=$target->fetch(PDO::FETCH_ASSOC);if(!is_array($answer))return 1;
        $before=$this->pdo->prepare("SELECT COUNT(*) FROM answers WHERE question_id=:question_id AND status='published' AND (is_best>:is_best OR (is_best=:same_best AND (created_at<:created_at OR (created_at=:same_created_at AND id<:id))))");
        $before->execute(['question_id'=>$questionId,'is_best'=>(int)$answer['is_best'],'same_best'=>(int)$answer['is_best'],'created_at'=>(string)$answer['created_at'],'same_created_at'=>(string)$answer['created_at'],'id'=>$answerId]);
        return max(1,(int)ceil(((int)$before->fetchColumn()+1)/max(1,$perPage)));
    }

    private function message(string $key): string
    {
        $v=$this->translator->get('api.'.$key);return $v==='api.'.$key?$key:$v;
    }
}
