package kr.itn.itnhub.mattermost;

import com.fasterxml.jackson.databind.JsonNode;
import kr.itn.itnhub.config.MattermostProperties;
import kr.itn.itnhub.feed.FileRef;
import kr.itn.itnhub.feed.FileView;
import kr.itn.itnhub.feed.PostView;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestClient;
import org.springframework.web.client.RestClientException;

import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;

@Component
public class MattermostRestClient implements MattermostClient {

    private final RestClient rest;
    private final String teamId;

    /**
     * 사용자 id → 표시용 이름(닉네임 우선, 없으면 아이디) 캐시. 게시물/자료 목록 조회 시
     * 같은 몇 명이 계속 반복해서 등장하므로, 매 게시글마다 {@code /users/{id}}를 다시
     * 부르지 않고 여기서 재사용한다. 조회 실패도 원본 id로 캐시해 전체 목록 조회가
     * 실패하지 않게 한다.
     */
    private final Map<String, String> userNameCache = new ConcurrentHashMap<>();

    @Autowired
    public MattermostRestClient(MattermostProperties props) {
        this(props, RestClient.builder());
    }

    /**
     * 전송 계층(transport)을 직접 지정할 수 있는 생성자. 운영 코드에서는 사용하지 않으며,
     * 테스트 등 별도의 {@link RestClient.Builder} 설정이 필요한 호출자를 위한 확장 지점이다.
     */
    MattermostRestClient(MattermostProperties props, RestClient.Builder builder) {
        this.teamId = props.teamId();
        this.rest = builder
                .baseUrl(props.baseUrl())
                .defaultHeader("Authorization", "Bearer " + props.token())
                .build();
    }

    @Override
    public Optional<String> findChannelIdByInternalName(String internalName) {
        try {
            JsonNode body = rest.get()
                    .uri("/api/v4/teams/{teamId}/channels/name/{name}", teamId, internalName)
                    .retrieve()
                    .onStatus(status -> status.value() == 404, (req, res) -> {
                        throw new NotFound();
                    })
                    .body(JsonNode.class);

            return idOf(body);
        } catch (NotFound e) {
            return Optional.empty();
        } catch (RestClientException e) {
            throw new MattermostException("채널 조회 실패: " + internalName, e);
        }
    }

    /** 팀 채널 목록 조회 시 한 페이지에 요청하는 최대 개수. */
    private static final int DISPLAY_NAME_PAGE_SIZE = 200;

    /**
     * 무한 루프 방지용 페이지 상한. VBA 원본의 안전장치를 그대로 옮긴 것으로,
     * 정상적인 팀이라면 도달할 일이 없다.
     */
    private static final int DISPLAY_NAME_MAX_PAGES = 100;

    /**
     * 표시명으로 채널을 찾는다. {@code /users/me/teams/{teamId}/channels}는 토큰 사용자가
     * "가입한" 채널만 돌려주므로 쓰지 않는다 - 이 조회는 레거시 채널(현재 내부명 규칙 이전에
     * 만들어져 내부명으로는 못 찾는 채널)을 복구하기 위한 것인데, 토큰 사용자가 그 채널에
     * 가입해 있지 않으면 조회가 누락되어 중복 채널이 생성된다. 대신 팀 전체 채널을
     * {@code /teams/{teamId}/channels}로 페이지네이션하며 훑는다 - 이 엔드포인트는 비공개
     * 채널까지 보려면 PAT가 system-admin 권한이어야 한다(README 참고).
     */
    @Override
    public Optional<String> findChannelIdByDisplayName(String displayName) {
        try {
            for (int page = 0; page < DISPLAY_NAME_MAX_PAGES; page++) {
                int currentPage = page;
                JsonNode channels = rest.get()
                        .uri(uri -> uri.path("/api/v4/teams/{teamId}/channels")
                                .queryParam("page", currentPage)
                                .queryParam("per_page", DISPLAY_NAME_PAGE_SIZE)
                                .build(teamId))
                        .retrieve()
                        .body(JsonNode.class);

                if (channels == null || !channels.isArray()) {
                    return Optional.empty();
                }

                for (JsonNode channel : channels) {
                    if (displayName.equals(channel.path("display_name").asText())) {
                        return idOf(channel);
                    }
                }

                if (channels.size() < DISPLAY_NAME_PAGE_SIZE) {
                    return Optional.empty();
                }
            }
            throw new MattermostException(
                    "채널 목록 조회 페이지 한도(" + DISPLAY_NAME_MAX_PAGES + ") 초과: " + displayName);
        } catch (RestClientException e) {
            throw new MattermostException("채널 목록 조회 실패", e);
        }
    }

    @Override
    public String createPrivateChannel(String internalName, String displayName) {
        Map<String, String> body = new LinkedHashMap<>();
        body.put("team_id", teamId);
        body.put("name", internalName);
        body.put("display_name", displayName);
        body.put("type", "P");
        body.put("purpose", "");
        body.put("header", "");

        try {
            JsonNode created = rest.post()
                    .uri("/api/v4/channels")
                    .contentType(MediaType.APPLICATION_JSON)
                    .body(body)
                    .retrieve()
                    .body(JsonNode.class);

            return idOf(created).orElseThrow(() ->
                    new MattermostException("채널 생성 응답에 id가 없습니다: " + displayName));
        } catch (RestClientException e) {
            throw new MattermostException("채널 생성 실패: " + displayName, e);
        }
    }

    /** {@code collectChannelFiles} 페이지네이션에 쓰는 페이지당 게시글 수. */
    private static final int FILES_PAGE_SIZE = 200;

    /** {@code findChannelIdByDisplayName}과 같은 이유의 무한 루프 방지 상한. */
    private static final int FILES_MAX_PAGES = 100;

    @Override
    public List<PostView> getRecentPosts(String channelId, int perPage) {
        try {
            JsonNode body = rest.get()
                    .uri(uri -> uri.path("/api/v4/channels/{channelId}/posts")
                            .queryParam("per_page", perPage)
                            .build(channelId))
                    .retrieve()
                    .body(JsonNode.class);

            if (body == null) {
                return List.of();
            }

            List<PostView> result = new ArrayList<>();
            JsonNode posts = body.path("posts");
            for (JsonNode idNode : body.path("order")) {
                result.add(toPostView(posts.path(idNode.asText())));
            }
            // Mattermost는 newest-first로 내려주지만 화면은 오래된순으로 보여준다.
            Collections.reverse(result);
            return result;
        } catch (RestClientException e) {
            throw new MattermostException("게시글 조회 실패: " + channelId, e);
        }
    }

    @Override
    public List<FileView> collectChannelFiles(String channelId) {
        List<FileView> files = new ArrayList<>();
        try {
            for (int page = 0; page < FILES_MAX_PAGES; page++) {
                int currentPage = page;
                JsonNode body = rest.get()
                        .uri(uri -> uri.path("/api/v4/channels/{channelId}/posts")
                                .queryParam("per_page", FILES_PAGE_SIZE)
                                .queryParam("page", currentPage)
                                .build(channelId))
                        .retrieve()
                        .body(JsonNode.class);

                if (body == null) {
                    return files;
                }

                JsonNode order = body.path("order");
                JsonNode posts = body.path("posts");
                int count = 0;
                for (JsonNode idNode : order) {
                    count++;
                    JsonNode post = posts.path(idNode.asText());
                    long createAt = post.path("create_at").asLong();
                    String uploader = resolveUserName(post.path("user_id").asText());
                    for (JsonNode f : post.path("metadata").path("files")) {
                        files.add(new FileView(
                                f.path("id").asText(),
                                f.path("name").asText(),
                                f.path("size").asLong(),
                                f.path("mime_type").asText(""),
                                createAt,
                                uploader));
                    }
                }

                if (count < FILES_PAGE_SIZE) {
                    return files;
                }
            }
            throw new MattermostException(
                    "채널 파일 목록 조회 페이지 한도(" + FILES_MAX_PAGES + ") 초과: " + channelId);
        } catch (RestClientException e) {
            throw new MattermostException("채널 파일 목록 조회 실패: " + channelId, e);
        }
    }

    @Override
    public byte[] downloadFile(String fileId) {
        try {
            byte[] body = rest.get()
                    .uri("/api/v4/files/{id}", fileId)
                    .retrieve()
                    .body(byte[].class);
            return body == null ? new byte[0] : body;
        } catch (RestClientException e) {
            throw new MattermostException("파일 다운로드 실패: " + fileId, e);
        }
    }

    @Override
    public FileRef fileInfo(String fileId) {
        try {
            JsonNode body = rest.get()
                    .uri("/api/v4/files/{id}/info", fileId)
                    .retrieve()
                    .body(JsonNode.class);

            if (body == null) {
                throw new MattermostException("파일 정보 조회 실패: " + fileId);
            }
            return new FileRef(fileId,
                    body.path("name").asText(""),
                    body.path("size").asLong(),
                    body.path("mime_type").asText(""));
        } catch (RestClientException e) {
            throw new MattermostException("파일 정보 조회 실패: " + fileId, e);
        }
    }

    private PostView toPostView(JsonNode post) {
        String id = post.path("id").asText();
        String message = post.path("message").asText("");
        long createAt = post.path("create_at").asLong();
        boolean system = post.path("type").asText("").startsWith("system_");

        List<FileRef> files = new ArrayList<>();
        for (JsonNode f : post.path("metadata").path("files")) {
            files.add(new FileRef(
                    f.path("id").asText(),
                    f.path("name").asText(),
                    f.path("size").asLong(),
                    f.path("mime_type").asText("")));
        }

        return new PostView(id, resolveUserName(post.path("user_id").asText()), message,
                createAt, system, files);
    }

    /**
     * 표시 이름 우선순위는 Mattermost 화면과 같게 맞춘다:
     * 닉네임 → 성명(first+last) → 계정명(username) → id.
     * (계정명만 쓰면 화면에 itnadmin처럼 나와 실제 Mattermost 표시와 어긋난다.)
     * 실패해도 원본 id로 캐시해, 사용자 조회 실패 하나 때문에 목록 전체가 죽지 않게 한다.
     */
    private String resolveUserName(String userId) {
        return userNameCache.computeIfAbsent(userId, id -> {
            try {
                JsonNode user = rest.get()
                        .uri("/api/v4/users/{id}", id)
                        .retrieve()
                        .body(JsonNode.class);
                if (user == null) {
                    return id;
                }
                String nickname = user.path("nickname").asText("");
                if (!nickname.isBlank()) {
                    return nickname;
                }
                String fullName = (user.path("first_name").asText("") + " "
                        + user.path("last_name").asText("")).trim();
                if (!fullName.isBlank()) {
                    return fullName;
                }
                String username = user.path("username").asText("");
                return username.isBlank() ? id : username;
            } catch (RestClientException e) {
                return id;
            }
        });
    }

    private Optional<String> idOf(JsonNode node) {
        if (node == null) {
            return Optional.empty();
        }
        String id = node.path("id").asText("");
        return id.isBlank() ? Optional.empty() : Optional.of(id);
    }

    /** 404를 정상 흐름으로 되돌리기 위한 내부 신호. 밖으로 새지 않는다. */
    private static final class NotFound extends RuntimeException {
    }
}
